<?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>Spring on David Parry</title>
    <link>https://davidparry.com/tags/spring/</link>
    <description>Recent content in Spring on David Parry</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 16 Jul 2026 09:00:00 -0500</lastBuildDate>
    <atom:link href="https://davidparry.com/tags/spring/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>🤖 From Prompting to Planning: What Embabel Taught Me About Agents</title>
      <link>https://davidparry.com/blog/2026/07/16/from-prompting-to-planning-what-embabel-taught-me-about-agents/</link>
      <pubDate>Thu, 16 Jul 2026 09:00:00 -0500</pubDate>
      <guid>https://davidparry.com/blog/2026/07/16/from-prompting-to-planning-what-embabel-taught-me-about-agents/</guid>
      <description>&lt;p&gt;&lt;img src=&#34;https://davidparry.com/images/goap.svg&#34; alt=&#34;Goal-Oriented Action Planning: typed results accumulate on the blackboard until the goal is reachable&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;I first ran into Embabel at DevNexus and have been quietly using it ever since, but it wasn&amp;rsquo;t until a recent class — going deep with Dashaun — that it really hit home. I spent the last stretch working through an Embabel workshop — building a bounded &amp;ldquo;digital worker&amp;rdquo; that responds to production incidents — and it reorganized how I think about agents on the JVM. Most of the agent content I read treats the LLM as the brain: you write a clever prompt, hand the model some tools, and hope it strings them together. Embabel pushes the intelligence somewhere far more boring and far more trustworthy: into Java types, into a planner, and into policy that a compiler and a test suite can see. This post is my attempt to write down what I actually learned, how I&amp;rsquo;d classify Embabel, and why I&amp;rsquo;m setting aside my own platform, AgentFabric, and starting to use Embabel instead.&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;&lt;img src=&#34;https://davidparry.com/images/goap.svg&#34; alt=&#34;Goal-Oriented Action Planning: typed results accumulate on the blackboard until the goal is reachable&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;I first ran into Embabel at DevNexus and have been quietly using it ever since, but it wasn&amp;rsquo;t until a recent class — going deep with Dashaun — that it really hit home. I spent the last stretch working through an Embabel workshop — building a bounded &amp;ldquo;digital worker&amp;rdquo; that responds to production incidents — and it reorganized how I think about agents on the JVM. Most of the agent content I read treats the LLM as the brain: you write a clever prompt, hand the model some tools, and hope it strings them together. Embabel pushes the intelligence somewhere far more boring and far more trustworthy: into Java types, into a planner, and into policy that a compiler and a test suite can see. This post is my attempt to write down what I actually learned, how I&amp;rsquo;d classify Embabel, and why I&amp;rsquo;m setting aside my own platform, AgentFabric, and starting to use Embabel instead.&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-one-sentence-reframe&#34;&gt;The one-sentence reframe&lt;/h3&gt;&#xA;&lt;p&gt;The line from the workshop that stuck with me was: &lt;em&gt;the worker chooses the path, your code defines the world.&lt;/em&gt; That is the whole shift. In a normal service you write a method that calls four collaborators in a fixed order. In Embabel you declare the capabilities and the desired outcome, and a planner discovers the order at runtime from the current state. You stop writing the sequence and start describing the world the sequence lives in.&lt;/p&gt;&#xA;&lt;h3 id=&#34;what-embabel-actually-is&#34;&gt;What Embabel actually is&lt;/h3&gt;&#xA;&lt;p&gt;If I had to classify Embabel in one phrase, I&amp;rsquo;d call it a &lt;strong&gt;neuro-symbolic, planning-first agent framework for the JVM&lt;/strong&gt;. Let me unpack why, because each word is doing work.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s &lt;strong&gt;symbolic&lt;/strong&gt; because the core engine is Goal-Oriented Action Planning (GOAP) — the same technique game AI has used for years. GOAP starts from the goal and reasons about which declared actions, given the current state, can reach it. The planner is deterministic. It is &lt;em&gt;not&lt;/em&gt; the LLM. This is the part people miss: Embabel does not ask a model &amp;ldquo;what should I do next?&amp;rdquo; It computes the plan from types.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s &lt;strong&gt;neural&lt;/strong&gt; because an individual action is free to call a model. The LLM lives &lt;em&gt;inside&lt;/em&gt; a step, boxed in by the types around it, not sitting above the whole process pulling levers.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s &lt;strong&gt;planning-first&lt;/strong&gt; because the mental model is OODA — Observe, Orient, Decide, Act — running as a loop. After every action produces a result (or fails), the planner re-observes the world and reassesses what is now possible. Failure isn&amp;rsquo;t an exception to swallow; it&amp;rsquo;s new information that changes the next plan.&lt;/p&gt;&#xA;&lt;p&gt;And it&amp;rsquo;s &lt;strong&gt;JVM-native&lt;/strong&gt; because Spring still owns everything. Embabel is a Kotlin framework with clean Java authoring, and your agent is still an ordinary &lt;code&gt;@Component&lt;/code&gt;. Constructor injection, interfaces, mocks, tests, observability — none of it changes. Embabel just adds planning metadata on top of methods you&amp;rsquo;d have written anyway.&lt;/p&gt;&#xA;&lt;h3 id=&#34;types-are-preconditions-and-effects&#34;&gt;Types are preconditions and effects&lt;/h3&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s the idea that made it click for me. Consider four action signatures from the incident worker:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ServiceObservation &lt;span style=&#34;color:#a6e22e&#34;&gt;observeServices&lt;/span&gt;(IncidentRequest request)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;RunbookAssessment &lt;span style=&#34;color:#a6e22e&#34;&gt;applyRunbook&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    IncidentRequest request, ServiceObservation observation)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;IncidentResponseReport &lt;span style=&#34;color:#a6e22e&#34;&gt;analyzeIncident&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    IncidentRequest request,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ServiceObservation observation,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    RunbookAssessment assessment)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;IncidentWorkflowReport &lt;span style=&#34;color:#a6e22e&#34;&gt;prepareReport&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    IncidentRequest request,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ServiceObservation observation,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    RunbookAssessment assessment,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    IncidentResponseReport response)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nobody writes the orchestration. The &lt;strong&gt;parameters are the preconditions&lt;/strong&gt; and the &lt;strong&gt;return type is the effect&lt;/strong&gt;. An action is applicable only when every parameter type already exists on the blackboard; running it deposits its return type, which unlocks the next action. So the plan isn&amp;rsquo;t authored — it &lt;em&gt;falls out&lt;/em&gt; of the data flow:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;IncidentRequest&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   └─ observeServices ─→ ServiceObservation&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        └─ applyRunbook ─→ RunbookAssessment&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;             └─ analyzeIncident ─→ IncidentResponseReport&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  └─ prepareReport ─→ IncidentWorkflowReport  (GOAL)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At the start only &lt;code&gt;IncidentRequest&lt;/code&gt; exists, so only &lt;code&gt;observeServices&lt;/code&gt; can fire. Each result makes exactly one more action eligible. I checked this against the workshop&amp;rsquo;s tiny planner, and the mechanism really is that literal: it filters methods whose parameter types are all present, picks the lowest-cost one, invokes it, and puts the result back on the blackboard. That&amp;rsquo;s it.&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-blackboard-is-typed-working-memory&#34;&gt;The blackboard is typed working memory&lt;/h3&gt;&#xA;&lt;p&gt;There&amp;rsquo;s no JSON router and no stringly-typed state machine. State is a set of domain objects — &lt;code&gt;IncidentRequest&lt;/code&gt;, &lt;code&gt;ServiceObservation&lt;/code&gt;, &lt;code&gt;RunbookAssessment&lt;/code&gt; — that the debugger, the compiler, the tests, the logs, and the planner all see identically. When I&amp;rsquo;ve built agent-ish things in the past, the &amp;ldquo;state&amp;rdquo; was usually a bag of strings passed through prompts, and it was untestable by construction. Making state a typed blackboard means the invariants live in Java, not in prose I&amp;rsquo;m begging a model to respect.&lt;/p&gt;&#xA;&lt;h3 id=&#34;dice-context-is-a-domain-model-not-a-prompt&#34;&gt;DICE: context is a domain model, not a prompt&lt;/h3&gt;&#xA;&lt;p&gt;The workshop calls this DICE — Domain-Integrated Context Engineering — and it&amp;rsquo;s the philosophical core. Instead of stuffing everything into a prompt, you encode organizational knowledge as executable, testable Java &lt;em&gt;first&lt;/em&gt;, then hand the model only the facts it needs to reason inside those boundaries.&lt;/p&gt;&#xA;&lt;p&gt;The clearest example is approval. The runbook, in plain Java, decides whether a proposed production change requires human sign-off:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt; (request.&lt;span style=&#34;color:#a6e22e&#34;&gt;incidentType&lt;/span&gt;()) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; OUT_OF_MEMORY &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; RunbookAssessment(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Heap pressure is consistent with an OutOfMemory failure.&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        evidence,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Capture a heap dump, roll back the latest risky change, &amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;then validate with a canary.&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;);   &lt;span style=&#34;color:#75715e&#34;&gt;// requiresApproval — always&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; HIGH_LATENCY &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; RunbookAssessment(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Database timeouts and cache misses indicate dependency saturation.&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        evidence,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Check database and cache health, then use an approved &amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rollback or scale-out.&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The model never sees a &lt;code&gt;requiresApproval&lt;/code&gt; field it could flip. It can&amp;rsquo;t. The model&amp;rsquo;s output type doesn&amp;rsquo;t contain that field, and the goal step copies the deterministic value straight from the runbook. Policy is the compiler&amp;rsquo;s job; interpretation is the model&amp;rsquo;s job. That separation is the whole point.&lt;/p&gt;&#xA;&lt;h3 id=&#34;where-spring-ai-comes-in&#34;&gt;Where Spring AI comes in&lt;/h3&gt;&#xA;&lt;p&gt;Spring AI is the thing doing the actual model call inside an action, and it&amp;rsquo;s exactly where I&amp;rsquo;ve spent most of my own time. The pattern is small and lovely:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; chatClient.&lt;span style=&#34;color:#a6e22e&#34;&gt;prompt&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;user&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    You are an SRE following a production runbook.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    Incident: %s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    Metrics: %s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    Logs: %s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    Runbook diagnosis: %s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    Approved strategy: %s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    Return concise analysis, diagnosis, and recommendation.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    Keep the recommendation inside the approved strategy.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;formatted&lt;/span&gt;(&lt;span style=&#34;color:#75715e&#34;&gt;/* domain facts */&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;call&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;entity&lt;/span&gt;(IncidentResponseReport.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;.entity(IncidentResponseReport.class)&lt;/code&gt; is the part I lean on constantly: the model reasons, but Java owns the schema. The reply comes back as a typed object or the action fails — and because it can fail, the plan needs a way to recover.&lt;/p&gt;&#xA;&lt;h3 id=&#34;failure-is-just-another-node-in-the-graph&#34;&gt;Failure is just another node in the graph&lt;/h3&gt;&#xA;&lt;p&gt;This was my favorite lesson, and it&amp;rsquo;s where planning earns its keep. The model call is the &lt;em&gt;preferred&lt;/em&gt; path (low cost). If it throws — Ollama cold, provider down, invalid output — the planner doesn&amp;rsquo;t retry the same prompt and it doesn&amp;rsquo;t ask the model to improvise. It re-observes the blackboard, notices the request and runbook assessment are still true, and selects a &lt;strong&gt;different declared capability&lt;/strong&gt; with the same effect type:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Action&lt;/span&gt;(description &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Fall back to deterministic runbook output&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        readOnly &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;, cost &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; 10.&lt;span style=&#34;color:#a6e22e&#34;&gt;0&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; IncidentResponseReport &lt;span style=&#34;color:#a6e22e&#34;&gt;fallBackToRunbook&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        IncidentRequest request, RunbookAssessment assessment) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; IncidentResponseReport(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;The model was unavailable; deterministic policy was retained.&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        assessment.&lt;span style=&#34;color:#a6e22e&#34;&gt;diagnosis&lt;/span&gt;(),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        assessment.&lt;span style=&#34;color:#a6e22e&#34;&gt;recommendedAction&lt;/span&gt;());&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two actions, same return type. Cost expresses preference (&lt;code&gt;1.0&lt;/code&gt; for the model, &lt;code&gt;10.0&lt;/code&gt; for the fallback) without a hand-written &lt;code&gt;if/else&lt;/code&gt; route. The blackboard makes the fallback applicable &lt;em&gt;only after&lt;/em&gt; the preferred path fails. Plan repair, expressed as data rather than control flow. And the whole run explains itself afterward: a &lt;code&gt;PlanExecution&lt;/code&gt; record lists completed actions, failed actions, and whether the goal was achieved — an audit trail of decisions and outcomes, not a dump of private chain-of-thought.&lt;/p&gt;&#xA;&lt;h3 id=&#34;autonomy-only-means-anything-inside-boundaries&#34;&gt;Autonomy only means anything inside boundaries&lt;/h3&gt;&#xA;&lt;p&gt;The worker is &lt;em&gt;allowed&lt;/em&gt; to observe Compose, apply the runbook, ask the model, fall back, and prepare a report. It is &lt;em&gt;not allowed&lt;/em&gt; to invent shell commands, restart production, bypass approval, or run forever. Two guardrails enforce the last one: actions are &lt;code&gt;FIRE_ONCE&lt;/code&gt; by default (no silent re-running) and the planner has a hard step limit. A read-only worker that stops at human approval is still autonomous — autonomy is goal-directed selection inside a capability boundary, not unrestricted mutation. That framing alone is worth the price of admission.&lt;/p&gt;&#xA;&lt;h3 id=&#34;why-im-putting-agentfabric-down-and-picking-up-embabel&#34;&gt;Why I&amp;rsquo;m putting AgentFabric down and picking up Embabel&lt;/h3&gt;&#xA;&lt;p&gt;The reason all of this landed so hard is that I&amp;rsquo;ve spent a long time building &lt;a href=&#34;https://github.com/davidparry/AgentFabric&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;AgentFabric&lt;/a&gt;&#xA; — my own durable JVM agent platform on the Spring AI substrate — and Embabel put a name to the philosophy I&amp;rsquo;d been reaching for by feel. Having seen it done properly, I&amp;rsquo;m going to stop investing in AgentFabric and start using Embabel instead.&lt;/p&gt;&#xA;&lt;p&gt;AgentFabric was my attempt to run agents in production, durably, across service boundaries. It stands on:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Spring AI&lt;/strong&gt; as the model and tool layer — &lt;code&gt;ChatClient&lt;/code&gt;, structured-output binding, and MCP tool calling wired through a &lt;code&gt;ToolCallbackProvider&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;LangGraph4j&lt;/strong&gt; for stateful graph topology — nodes that call the model, routers that branch on conditions, and verification loops that re-prompt until output passes a quality gate.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Temporal&lt;/strong&gt; for durable execution — every graph run is a workflow, so a crashed agent resumes from the last completed node instead of replaying LLM calls.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A2A and MCP&lt;/strong&gt; as the wire protocols — agent-to-agent messaging and agent-to-tool calls.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; as the source of truth for configuration, checkpoints, and token accounting.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;It works. But being honest with myself, most of that is orchestration plumbing I wrote so I could get to the actual agent — and the hardest, most valuable part, the &lt;em&gt;deliberation&lt;/em&gt;, is the part I did worst. In AgentFabric I describe the graph — nodes, edges, routers — as declarative topology, which means I&amp;rsquo;m still hand-authoring the sequence and then maintaining it forever. Embabel&amp;rsquo;s whole point is that I shouldn&amp;rsquo;t be doing that at all: declare capabilities as typed actions and a goal, and the planner &lt;em&gt;derives&lt;/em&gt; the topology from data flow. The centerpiece of my platform turns out to be a worse version of something a maintained framework already gives me for free.&lt;/p&gt;&#xA;&lt;p&gt;What convinced me isn&amp;rsquo;t that Embabel is different — it&amp;rsquo;s that everything I got right in AgentFabric, I got right by accidentally reinventing Embabel, badly:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;My &lt;strong&gt;LangGraph4j state channels&lt;/strong&gt; are a home-grown &lt;strong&gt;blackboard&lt;/strong&gt; — except in Embabel the &lt;em&gt;presence&lt;/em&gt; of a type is what makes the next step applicable, so I stop drawing edges entirely.&lt;/li&gt;&#xA;&lt;li&gt;My &lt;strong&gt;verifier loop plus deterministic fallback&lt;/strong&gt; is hand-wired &lt;strong&gt;plan repair&lt;/strong&gt; — Embabel does it with a second declared action at a higher cost, selected automatically when the model action fails. No re-prompt path to maintain.&lt;/li&gt;&#xA;&lt;li&gt;My &lt;strong&gt;&lt;code&gt;completeAs(Class&amp;lt;T&amp;gt;)&lt;/code&gt;&lt;/strong&gt; calls are just Embabel&amp;rsquo;s &lt;strong&gt;&lt;code&gt;.entity(...)&lt;/code&gt;&lt;/strong&gt; — both are Spring AI underneath, so this is the one place there&amp;rsquo;s genuinely nothing to migrate; it&amp;rsquo;s the same call.&lt;/li&gt;&#xA;&lt;li&gt;My &lt;strong&gt;budget guard&lt;/strong&gt; router is a clumsier &lt;strong&gt;cost-based action preference&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;li&gt;My &lt;strong&gt;guarded write tools&lt;/strong&gt; (dry-run unless &lt;code&gt;apply=true&lt;/code&gt;) are the same instinct as keeping &lt;code&gt;requiresApproval&lt;/code&gt; in Java, never the model — one thing I&amp;rsquo;ll happily carry over as a habit rather than a codebase.&lt;/li&gt;&#xA;&lt;li&gt;Even the &lt;strong&gt;durability&lt;/strong&gt; I reached for Temporal to get is largely native: Embabel&amp;rsquo;s blackboard persists through a pluggable &lt;code&gt;AgentProcessRepository&lt;/code&gt; — in-memory by default, but back it with &lt;strong&gt;PostgreSQL or MongoDB&lt;/strong&gt; and a process survives a restart. And &lt;strong&gt;human-in-the-loop pause/resume&lt;/strong&gt; is built in via awaitables: a process returns &lt;code&gt;WAITING&lt;/code&gt; with a &lt;code&gt;processId&lt;/code&gt; and you resume it later through &lt;code&gt;/continue&lt;/code&gt;. I wrote a Temporal integration to get exactly these two things.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;There&amp;rsquo;s a real cost to admitting this — AgentFabric is a lot of my work — but continuing to maintain a bespoke orchestration engine to avoid adopting a better, supported one is just ego with a build file. Embabel gives me the planner; Spring gives me the beans; Spring AI gives me the model calls I already knew; a persistent repository gives me durable state and pause/resume.&lt;/p&gt;&#xA;&lt;p&gt;So do I still need Temporal? Almost never. The &lt;strong&gt;only&lt;/strong&gt; case that would pull it back in is when I need guarantees Embabel&amp;rsquo;s step-level persistence doesn&amp;rsquo;t offer: deterministic &lt;strong&gt;replay with exactly-once activities&lt;/strong&gt; (the JVM dies mid-LLM-call and must resume &lt;em&gt;without&lt;/em&gt; re-invoking the model), &lt;strong&gt;durable timers&lt;/strong&gt; (&amp;ldquo;wait three days, then continue&amp;rdquo;), or &lt;strong&gt;distributed orchestration&lt;/strong&gt; across a worker fleet with guaranteed delivery and backoff. For a single-process agent that plans, calls a model, and pauses for human approval — which is most of what I actually build — none of that applies. So AgentFabric goes on the shelf, and my next agent starts as an Embabel &lt;code&gt;@Agent&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;And with the recent news that Embabel is heading for a &lt;code&gt;1.0.0&lt;/code&gt; release, whatever hesitation I had about betting on it is gone. I&amp;rsquo;m all in.&lt;/p&gt;&#xA;&lt;h3 id=&#34;what-im-taking-away&#34;&gt;What I&amp;rsquo;m taking away&lt;/h3&gt;&#xA;&lt;p&gt;The move Embabel names is the move from &lt;strong&gt;calling APIs to building workers&lt;/strong&gt;. A worker navigates a typed domain, invokes real Spring-managed services, plans from preconditions and effects, stops at an explicit goal, repairs a failed path, preserves human approval, and leaves an audit trail. None of that requires trusting a model with the steering wheel. It requires giving the model a small, well-lit room to think in — and letting typed code define everything outside the door. That&amp;rsquo;s the bet I&amp;rsquo;m making by setting my own platform down and building on Embabel instead.&lt;/p&gt;&#xA;&lt;p&gt;If you&amp;rsquo;re on the JVM and you&amp;rsquo;ve been building agents by growing ever-larger prompts, I&amp;rsquo;d genuinely recommend sitting with GOAP for an afternoon. It reframes the problem from &amp;ldquo;how do I make the model behave&amp;rdquo; to &amp;ldquo;what world am I asking it to operate in&amp;rdquo; — and the second question is one your compiler can help you answer.&lt;/p&gt;&#xA;</content:encoded>
    </item>
  </channel>
</rss>
