<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Solon Framework</title>
    <description>The latest articles on DEV Community by Solon Framework (@solonjava).</description>
    <link>https://dev.to/solonjava</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4003833%2F83933c1e-7d66-44d1-9237-16669f6b9a80.png</url>
      <title>DEV Community: Solon Framework</title>
      <link>https://dev.to/solonjava</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/solonjava"/>
    <language>en</language>
    <item>
      <title>Rerun and Continue in SolonCode Web — Two Buttons, Two Semantics</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 24 Aug 2026 07:38:50 +0000</pubDate>
      <link>https://dev.to/solonjava/rerun-and-continue-in-soloncode-web-two-buttons-two-semantics-8c1</link>
      <guid>https://dev.to/solonjava/rerun-and-continue-in-soloncode-web-two-buttons-two-semantics-8c1</guid>
      <description>&lt;p&gt;When you're chatting with an AI agent, two things go wrong most often: the model produced an answer you don't want, or the model stopped too early and you'd like it to keep going.&lt;/p&gt;

&lt;p&gt;SolonCode's Web UI solves both problems with two small icons — &lt;strong&gt;Re-run&lt;/strong&gt; and &lt;strong&gt;Continue&lt;/strong&gt; — that sit next to every AI response bubble. They look alike (same toolbar, same spot) but do very different things under the hood.&lt;/p&gt;

&lt;p&gt;This article walks through what each button does, what the code does when you click it, and when you should use one versus the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two buttons at a glance
&lt;/h2&gt;

&lt;p&gt;Every AI response row in SolonCode Web shows four action icons to its right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy&lt;/strong&gt; — copies the final answer to the clipboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run&lt;/strong&gt; (循环箭头) — replays the last user message from scratch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continue&lt;/strong&gt; (快进) — extends the last AI response without deleting anything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete&lt;/strong&gt; — removes the response and everything after it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Re-run and Continue buttons are the focus here. Both send a command to the engine (&lt;code&gt;/rerun&lt;/code&gt; and &lt;code&gt;/continue&lt;/code&gt;), but the effect on the conversation is fundamentally different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-run (重新运行): replay the turn, delete the old answer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you see
&lt;/h3&gt;

&lt;p&gt;You type "Refactor &lt;code&gt;OrderRepository&lt;/code&gt; to use Solon Data" and the agent returns a response with code changes. You open the result and decide the approach is wrong — maybe it touched too many files, or used the wrong pattern. You click the &lt;strong&gt;Re-run&lt;/strong&gt; icon on that AI response.&lt;/p&gt;

&lt;p&gt;The old AI bubble vanishes. A new one appears in its place, and the agent starts thinking again.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the code does
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;RerunCommand&lt;/code&gt; (&lt;code&gt;org.noear.solon.codecli.command.builtin.RerunCommand&lt;/code&gt;) implements the command:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Walk backwards through the session message list until it finds a &lt;code&gt;UserMessage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Strip that user message and everything after it from the session (&lt;code&gt;session.removeLatestMessage(1)&lt;/code&gt; in a loop).&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;ctx.runAgentTask(lastUserInput, null)&lt;/code&gt; — the same input, a clean slate.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RerunCommand.java (simplified)&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;messageList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessages&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;lastUserInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;messageList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ChatMessage&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messageList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;lastUserInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removeLatestMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removeLatestMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// strip AI messages too&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runAgentTask&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastUserInput&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Web UI mirrors this by removing every DOM element with the same &lt;code&gt;data-run-id&lt;/code&gt; — the AI bubble, any tool cards, thinking blocks, everything from that turn — so the new response renders in a clean bubble.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to use Re-run
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The agent's last answer is wrong and you want to try again with the same prompt&lt;/li&gt;
&lt;li&gt;The agent took a wrong turn mid-response and you want it to pick a different path&lt;/li&gt;
&lt;li&gt;You want to test whether a different model or a different system prompt would give a better result on the same input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key property&lt;/strong&gt;: Re-run preserves your conversation history &lt;em&gt;up to&lt;/em&gt; the last user message, then replays that message. Everything after the user message is discarded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue (继续运行): extend the response, keep what's there
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you see
&lt;/h3&gt;

&lt;p&gt;The agent responded with a partial refactor — it changed &lt;code&gt;OrderRepository&lt;/code&gt; but stopped mid-way through &lt;code&gt;PaymentService&lt;/code&gt;. You click &lt;strong&gt;Continue&lt;/strong&gt; and the same bubble stays; new content simply flows into it, like the agent kept typing.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the code does
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ContinueCommand&lt;/code&gt; (&lt;code&gt;org.noear.solon.codecli.command.builtin.ContinueCommand&lt;/code&gt;) takes a different approach. Instead of discarding the last turn, it manipulates the agent's internal execution trace:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the &lt;code&gt;ReActTrace&lt;/code&gt; stored in the session context under &lt;code&gt;"__main"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the trace's current route is &lt;code&gt;Agent.ID_END&lt;/code&gt; (meaning the agent has already reached its final answer node), reset the route back to &lt;code&gt;ReActAgent.ID_REASON&lt;/code&gt; — putting the agent back in "thinking" mode.&lt;/li&gt;
&lt;li&gt;Clear the final answer from the trace (&lt;code&gt;trace.setFinalAnswer(null, false)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Remove the last assistant message from working memory and from the session (so it will be regenerated).&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;ctx.runAgentTask(null, null)&lt;/code&gt; — the agent picks up from where it left off.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ContinueCommand.java (simplified)&lt;/span&gt;
&lt;span class="nc"&gt;ReActTrace&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getAs&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"__main"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ID_END&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRoute&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setRoute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ID_REASON&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// go back to thinking&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setFinalAnswer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// clear the old answer&lt;/span&gt;

        &lt;span class="nc"&gt;ChatMessage&lt;/span&gt; &lt;span class="n"&gt;workMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWorkingMemory&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getLastMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workMessage&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;AssistantMessage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWorkingMemory&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;removeLastMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;messageList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessages&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageList&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;messageList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messageList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;AssistantMessage&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removeLatestMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// remove the completed reply&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runAgentTask&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// continue from current state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the trace is reset to the reasoning step, the agent doesn't start a new conversation — it continues the same reasoning chain, appending to the existing response.&lt;/p&gt;

&lt;p&gt;On the Web UI side, the Continue button sends &lt;code&gt;/continue&lt;/code&gt; with &lt;code&gt;removeRow=false&lt;/code&gt;, so the existing bubble stays visible and new content simply appends to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to use Continue
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The agent stopped mid-task (e.g., refactored 3 of 12 files) and you want it to keep going&lt;/li&gt;
&lt;li&gt;The agent's last answer was mostly right but incomplete&lt;/li&gt;
&lt;li&gt;You want the response to feel like a natural extension, not a re-do&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key property&lt;/strong&gt;: Continue does not discard anything. It preserves the conversation history and the agent's reasoning state, then asks the agent to pick up where it stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-side comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Re-run&lt;/th&gt;
&lt;th&gt;Continue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Command&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rerun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/continue&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deletes old response?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — removes the entire run&lt;/td&gt;
&lt;td&gt;No — appends to the same bubble&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Strips history?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — removes everything after the last user message&lt;/td&gt;
&lt;td&gt;No — preserves all history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resets agent state?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — starts fresh with the same prompt&lt;/td&gt;
&lt;td&gt;Partially — resets trace to reasoning step, keeps context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Preserves tools/skills?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — same session, same tools&lt;/td&gt;
&lt;td&gt;Yes — same session, same tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"That answer was wrong, try again"&lt;/td&gt;
&lt;td&gt;"That answer was incomplete, keep going"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web UI icon&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Loop arrow (循环箭头)&lt;/td&gt;
&lt;td&gt;Fast-forward (快进)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;i18n key&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;msg.rerun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;msg.continue&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chinese label&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;重新运行&lt;/td&gt;
&lt;td&gt;继续运行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;Imagine you ask SolonCode: &lt;em&gt;"Migrate the &lt;code&gt;UserService&lt;/code&gt; from Spring Data JPA to Solon Data."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario A — the agent got the migration wrong&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent produced a response that uses the wrong Solon Data API. You click &lt;strong&gt;Re-run&lt;/strong&gt;. The old response disappears, the agent thinks again, and this time produces a correct migration. Your history now has the corrected response in place of the old one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario B — the agent stopped halfway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent migrated &lt;code&gt;findUserById&lt;/code&gt; but stopped before &lt;code&gt;findByEmail&lt;/code&gt;. You click &lt;strong&gt;Continue&lt;/strong&gt;. The same bubble stays, and the agent keeps typing — now adding &lt;code&gt;findByEmail&lt;/code&gt; to the response. Nothing is deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario C — the agent hallucinated a dependency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent's response included a &lt;code&gt;@Component&lt;/code&gt; on a class that shouldn't have one. You click &lt;strong&gt;Re-run&lt;/strong&gt;. The agent re-thinks and this time produces a cleaner response. Then you realize the response is still missing &lt;code&gt;deleteById&lt;/code&gt;, so you click &lt;strong&gt;Continue&lt;/strong&gt; to add it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the commands are exposed
&lt;/h2&gt;

&lt;p&gt;Both &lt;code&gt;/rerun&lt;/code&gt; and &lt;code&gt;/continue&lt;/code&gt; are registered as built-in commands (since v2026.4.28):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt;: type &lt;code&gt;/rerun&lt;/code&gt; or &lt;code&gt;/continue&lt;/code&gt; directly in the terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web UI&lt;/strong&gt;: click the icon on any AI response bubble&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IM channels&lt;/strong&gt; (飞书/钉钉/微信): send &lt;code&gt;/rerun&lt;/code&gt; or &lt;code&gt;/continue&lt;/code&gt; in chat — the IM bot forwards the same command to the engine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the Web UI, &lt;code&gt;RerunCommand&lt;/code&gt; and &lt;code&gt;ContinueCommand&lt;/code&gt; are not marked &lt;code&gt;cliOnly()&lt;/code&gt;, so they appear in the &lt;code&gt;/web/chat/hints&lt;/code&gt; endpoint and are available from any channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood: the runId linkage
&lt;/h2&gt;

&lt;p&gt;One detail worth understanding is how the Web UI knows which elements belong to a single "run."&lt;/p&gt;

&lt;p&gt;Every message element — the AI bubble, tool cards, thinking blocks — is stamped with &lt;code&gt;data-run-id&lt;/code&gt; (the same ID assigned to the current &lt;code&gt;ReActTrace&lt;/code&gt;). When you click Re-run, the UI removes all elements with that &lt;code&gt;runId&lt;/code&gt; from the DOM before the new response arrives. This is why the entire turn vanishes atomically, not just the text bubble.&lt;/p&gt;

&lt;p&gt;Continue doesn't touch the DOM at all — it just sends the command and lets the stream append to the existing bubble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Re-run and Continue are two sides of the same coin: &lt;strong&gt;Re-run is about correctness&lt;/strong&gt; (throw away a bad answer and try again), while &lt;strong&gt;Continue is about completeness&lt;/strong&gt; (keep a partial answer and finish it).&lt;/p&gt;

&lt;p&gt;Both operate at the session level — they don't create new sessions, they don't change tools or models, and they don't affect other conversations. The only difference is whether the old response gets discarded (rerun) or preserved (continue).&lt;/p&gt;

&lt;p&gt;Next time you're chatting with SolonCode and the agent's answer isn't quite right, reach for the icon that matches your intent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong?&lt;/strong&gt; → Re-run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete?&lt;/strong&gt; → Continue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are available in the Web UI, CLI, and IM channels — pick the one that fits your workflow.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;SolonCode is an open-source coding agent built on Solon AI and Java. It supports CLI, Web, Desktop, and ACP interaction modes. Open source under Apache 2.0 / MIT.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Spring AI vs Solon AI - Building Agents and Handling Agent Events</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:14:09 +0000</pubDate>
      <link>https://dev.to/solonjava/spring-ai-vs-solon-ai-building-agents-and-handling-agent-events-ncp</link>
      <guid>https://dev.to/solonjava/spring-ai-vs-solon-ai-building-agents-and-handling-agent-events-ncp</guid>
      <description>&lt;p&gt;Both Spring AI and Solon AI can build tool-calling, multi-step agents in Java. But once your agent grows beyond a single chat call — you want step-by-step streaming, per-tool tracing, run metrics, or human-in-the-loop suspension — the two frameworks diverge sharply in &lt;em&gt;how&lt;/em&gt; you assemble the agent and &lt;em&gt;how&lt;/em&gt; you observe its execution.&lt;/p&gt;

&lt;p&gt;This article compares the two side by side: agent construction first, then the event/observability models, with runnable examples for each. Everything below is checked against the official docs (Spring AI reference) and the Solon AI v4.0.5 source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Building Agents
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Spring AI: The Agent Is a Pattern, Not a Type
&lt;/h3&gt;

&lt;p&gt;Spring AI has &lt;strong&gt;no &lt;code&gt;Agent&lt;/code&gt; class&lt;/strong&gt;. An "agent" is a composition: a &lt;code&gt;ChatClient&lt;/code&gt; with advisors, plus tools, plus (optionally) memory. The tool-calling loop is run by the framework through the always-auto-registered &lt;code&gt;ToolCallingAdvisor&lt;/code&gt;, which delegates execution to &lt;code&gt;ToolCallingManager&lt;/code&gt; and loops until the model stops requesting tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultSystem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"You are a helpful weather assistant."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultTools&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeatherTools&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;          &lt;span class="c1"&gt;// @Tool-annotated methods&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;MessageChatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What's the weather in Hangzhou? Should I bring an umbrella?"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring AI also documents three tool-execution modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Framework-controlled&lt;/strong&gt; — the default above; you never see the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advisor-controlled&lt;/strong&gt; — you configure &lt;code&gt;ToolCallingAdvisor&lt;/code&gt; explicitly to customize the loop's stop condition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-controlled&lt;/strong&gt; — you disable the auto-registered advisor (&lt;code&gt;AdvisorParams.toolCallingAdvisorAutoRegister(false)&lt;/code&gt;) and drive the loop yourself, aggregating streamed chunks with &lt;code&gt;ChatClientMessageAggregator&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For &lt;strong&gt;multi-agent&lt;/strong&gt; orchestration, Spring AI's documented pattern is composition: one &lt;code&gt;ChatClient&lt;/code&gt; can be exposed as a &lt;code&gt;ToolCallback&lt;/code&gt; for another, so a "planner" agent literally calls a "worker" agent as a tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A worker agent exposed as a tool (documented pattern)&lt;/span&gt;
&lt;span class="nc"&gt;ToolCallback&lt;/span&gt; &lt;span class="n"&gt;researcherTool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FunctionToolCallback&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"researcher"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;researcherClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ask the researcher sub-agent"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inputType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The strength here is familiarity: if you know &lt;code&gt;ChatClient&lt;/code&gt; + advisors, you already know how to build agents. The weakness is that there is no first-class object representing "an agent run" — no run ID, no run-scoped trace, no built-in notion of an agent's lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solon AI: Agents Are First-Class Citizens
&lt;/h3&gt;

&lt;p&gt;Solon AI ships an actual agent layer in &lt;code&gt;solon-ai-agent&lt;/code&gt;. The root interface &lt;code&gt;Agent&lt;/code&gt; declares &lt;code&gt;name()&lt;/code&gt;, &lt;code&gt;role()&lt;/code&gt;, &lt;code&gt;profile()&lt;/code&gt;, and the request builders &lt;code&gt;prompt(...)&lt;/code&gt;. Out of the box you get three levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ReActAgent&lt;/code&gt;&lt;/strong&gt; — a single agent running a think → act → observe loop with tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TeamAgent&lt;/code&gt;&lt;/strong&gt; — a coordinator that plans and delegates to multiple member agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ai Flow&lt;/strong&gt; — YAML-declared workflows where agents are nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building a ReAct agent is a builder chain over the same portable &lt;code&gt;ChatModel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WeatherTools&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbsToolProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@ToolMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get current weather for a city"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getWeather&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"City name"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;weatherService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lookup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiUrl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"weather_helper"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Answers weather questions and gives clothing advice"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modelOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="no"&gt;F&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// low temp for consistent reasoning&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultToolAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeatherTools&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxTurns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;// hard cap on the reasoning loop&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WeatherAdvice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// structured final answer&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;AgentSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryAgentSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"session-001"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What's the weather in Hangzhou?"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things are worth noticing versus Spring AI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The loop has a budget&lt;/strong&gt; — &lt;code&gt;maxTurns&lt;/code&gt;, &lt;code&gt;retryConfig(maxRetries, retryDelayMs)&lt;/code&gt;, and &lt;code&gt;autoRethink&lt;/code&gt; are builder-level controls on the reasoning loop itself, not something you wire by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The session is the agent's memory&lt;/strong&gt; — &lt;code&gt;AgentSession&lt;/code&gt; carries the working memory and execution snapshot; it can be serialized to JSON (&lt;code&gt;FlowContext.toJson()&lt;/code&gt; / &lt;code&gt;fromJson()&lt;/code&gt;) and restored, which also powers human-in-the-loop: &lt;code&gt;session.pending(true, "waiting for approval")&lt;/code&gt; suspends mid-run, and &lt;code&gt;agent.prompt()&lt;/code&gt; (no argument) resumes it later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async and streaming are symmetric&lt;/strong&gt; — every agent request supports &lt;code&gt;.call()&lt;/code&gt;, &lt;code&gt;.callAsync()&lt;/code&gt; (CompletableFuture), and &lt;code&gt;.stream()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A &lt;code&gt;TeamAgent&lt;/code&gt; reuses the same builder shape, and notably &lt;strong&gt;can work without its own model&lt;/strong&gt; — &lt;code&gt;TeamAgent.of(null)&lt;/code&gt; builds a deterministic coordinator driven purely by code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TeamAgent&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TeamAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"support_team"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultInterceptorAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AuditInterceptor&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 2: Events and Observability
&lt;/h2&gt;

&lt;p&gt;This is where the architectural difference is deepest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spring AI: Observability via Micrometer, Not an Event Stream
&lt;/h3&gt;

&lt;p&gt;Spring AI's answer to "what happened inside my agent run" is the &lt;strong&gt;Micrometer Observation API&lt;/strong&gt;. &lt;code&gt;ChatClient&lt;/code&gt; calls and tool executions automatically record observations (&lt;code&gt;spring.ai.chat.client&lt;/code&gt;, &lt;code&gt;spring.ai.tool&lt;/code&gt;) with GenAI-convention key-values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gen_ai.operation.name&lt;/code&gt; (&lt;code&gt;execute_tool&lt;/code&gt; for tools)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spring.ai.tool.definition.name&lt;/code&gt;, &lt;code&gt;spring.ai.tool.call.id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tool call arguments and results are &lt;strong&gt;not exported by default&lt;/strong&gt; (sensitive data); opt in with &lt;code&gt;spring.ai.tools.observations.include-content=true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These feed tracing backends (Zipkin, Tempo, ...) via standard Spring Boot actuator configuration. It is production-grade telemetry — but it is &lt;em&gt;metrics and spans&lt;/em&gt;, not &lt;em&gt;events you can program against&lt;/em&gt;. There is no &lt;code&gt;RunStarted&lt;/code&gt; callback, no per-tool-call hook you can implement to mutate behavior, and no run object carrying token usage you can read in-process after a run.&lt;/p&gt;

&lt;p&gt;For streaming, you get &lt;code&gt;Flux&amp;lt;ChatResponse&amp;gt;&lt;/code&gt; content chunks; and if you drive the tool loop yourself, &lt;code&gt;ChatClientMessageAggregator&lt;/code&gt; lets you tap each iteration's chunk flux — but the lifecycle of "a tool was selected → executed → returned" surfaces only through observation spans.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solon AI: A Typed Event Stream Plus Lifecycle Interceptors
&lt;/h3&gt;

&lt;p&gt;Solon AI treats the agent run as a &lt;strong&gt;stream of typed events&lt;/strong&gt;. &lt;code&gt;AgentRequest.stream()&lt;/code&gt; returns &lt;code&gt;Flux&amp;lt;AgentEvent&amp;gt;&lt;/code&gt;, and every event carries &lt;code&gt;getRunId()&lt;/code&gt;, &lt;code&gt;getAgentName()&lt;/code&gt;, the &lt;code&gt;AgentSession&lt;/code&gt;, and a metadata map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What's the weather in Hangzhou?"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doOnNext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;RunStartEvent&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"run {} started on agent {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRunId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAgentName&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasContent&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// incremental token stream for the UI&lt;/span&gt;
                &lt;span class="n"&gt;uiSink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;RunEndEvent&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Metrics&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetrics&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"run finished: abnormal={}, tokens={}, duration={}ms"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAbnormal&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalTokens&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalDuration&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;})&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event vocabulary is small and structural:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RunStartEvent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an agent run begins (carries &lt;code&gt;ReActTrace&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;content events (&lt;code&gt;hasContent()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;streaming answer chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RunEndEvent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;run finished — carries the final &lt;code&gt;ReActResponse&lt;/code&gt;, the trace, and &lt;code&gt;Metrics&lt;/code&gt; (prompt/completion/total tokens, cache tokens, total duration, &lt;code&gt;isAbnormal()&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TeamStartEvent&lt;/code&gt; / &lt;code&gt;NodeStartEvent&lt;/code&gt; / &lt;code&gt;NodeChunk&lt;/code&gt; / &lt;code&gt;NodeEndEvent&lt;/code&gt; / &lt;code&gt;TeamEndEvent&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;team-level granularity: which member agent started, streamed, finished&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because events are typed objects on a Reactor flux, wiring them to SSE, WebSockets, or an audit log is trivial — no observation registry required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complementing the event stream is the interceptor chain.&lt;/strong&gt; &lt;code&gt;ReActInterceptor&lt;/code&gt; gives you synchronous hooks into every phase of the loop, registered per agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuditInterceptor&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ReActInterceptor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onReasonStart&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ReActTrace&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt; &lt;span class="n"&gt;systemPromptBuf&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRunId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// correlate with the event stream&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onToolCallStart&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ReActTrace&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ToolExchanger&lt;/span&gt; &lt;span class="n"&gt;toolExchanger&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"calling tool: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toolExchanger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToolName&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onToolCallEnd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ReActTrace&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ToolExchanger&lt;/span&gt; &lt;span class="n"&gt;toolExchanger&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                              &lt;span class="nc"&gt;ChatMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;durationMs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tool {} done in {}ms, error={}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;toolExchanger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToolName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;durationMs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onObservation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ReActTrace&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ToolExchanger&lt;/span&gt; &lt;span class="n"&gt;toolExchanger&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                              &lt;span class="nd"&gt;@Nullable&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt; &lt;span class="n"&gt;observation&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                              &lt;span class="nd"&gt;@Nullable&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;durationMs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// the observation that will be fed back to the model&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultToolAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeatherTools&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultInterceptorAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AuditInterceptor&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full hook surface: &lt;code&gt;onAgentStart&lt;/code&gt;, &lt;code&gt;onReasonStart&lt;/code&gt; / &lt;code&gt;onReasonEnd&lt;/code&gt;, &lt;code&gt;onPlan&lt;/code&gt;, &lt;code&gt;onActionStart&lt;/code&gt; / &lt;code&gt;onActionEnd&lt;/code&gt;, &lt;code&gt;onToolCallStart&lt;/code&gt; / &lt;code&gt;onToolCallEnd&lt;/code&gt;, &lt;code&gt;onAgentEnd&lt;/code&gt; — plus &lt;code&gt;onThought&lt;/code&gt; / &lt;code&gt;onAction&lt;/code&gt; / &lt;code&gt;onObservation&lt;/code&gt; mirroring the classic ReAct vocabulary. Since &lt;code&gt;ReActInterceptor&lt;/code&gt; extends both &lt;code&gt;AgentInterceptor&lt;/code&gt; and &lt;code&gt;ChatInterceptor&lt;/code&gt;, one object can observe agent-level and raw-model-level traffic simultaneously.&lt;/p&gt;

&lt;p&gt;And unlike observation spans, these hooks can &lt;em&gt;do&lt;/em&gt; things: mutate the system prompt buffer at &lt;code&gt;onReasonStart&lt;/code&gt;, redact tool results, enforce budgets, or suspend the run.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Philosophical Difference in One Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Spring AI&lt;/th&gt;
&lt;th&gt;Solon AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent abstraction&lt;/td&gt;
&lt;td&gt;none — &lt;code&gt;ChatClient&lt;/code&gt; + advisors pattern&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Agent&lt;/code&gt; / &lt;code&gt;ReActAgent&lt;/code&gt; / &lt;code&gt;TeamAgent&lt;/code&gt; types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool loop owner&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ToolCallingAdvisor&lt;/code&gt; (framework, advisor, or user-controlled)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ReActAgent&lt;/code&gt; loop with &lt;code&gt;maxTurns&lt;/code&gt; / &lt;code&gt;retryConfig&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-agent&lt;/td&gt;
&lt;td&gt;compose clients-as-tools&lt;/td&gt;
&lt;td&gt;built-in &lt;code&gt;TeamAgent&lt;/code&gt; coordinator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution events&lt;/td&gt;
&lt;td&gt;not exposed as API&lt;/td&gt;
&lt;td&gt;typed &lt;code&gt;Flux&amp;lt;AgentEvent&amp;gt;&lt;/code&gt; stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-tool hooks&lt;/td&gt;
&lt;td&gt;via Micrometer spans&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ReActInterceptor&lt;/code&gt; lifecycle callbacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token/cost per run&lt;/td&gt;
&lt;td&gt;from observation metrics export&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Metrics&lt;/code&gt; on &lt;code&gt;RunEndEvent&lt;/code&gt;, in-process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mid-run suspension&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;session.pending()&lt;/code&gt; + resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telemetry backend&lt;/td&gt;
&lt;td&gt;Micrometer / OTel, first-class&lt;/td&gt;
&lt;td&gt;read events/trace in code; bridge to whatever you like&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which One Fits?
&lt;/h2&gt;

&lt;p&gt;These models are not enemies — they answer different pressures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If your agents live inside a Spring estate&lt;/strong&gt; and your observability story is already Micrometer/tracing-based, Spring AI's pattern-based approach slots in with zero new concepts, and spans flow to your existing dashboards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you are building agent-centric products&lt;/strong&gt; — dashboards that render reasoning steps, billing driven by per-run token metrics, HITL approval flows, SSE streams of typed events — Solon AI's first-class run/event/interceptor model gives you that without wrapping observation infrastructure in application code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A pragmatic hybrid also works well and is worth stating plainly: Spring Boot as the shell (web, security, actuator) with Solon AI's agent kernel inside it — Solon AI is framework-neutral, so &lt;code&gt;ReActAgent&lt;/code&gt; runs happily as a plain bean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Spring AI: Tool Calling and Observability chapters — &lt;a href="https://docs.spring.io/spring-ai/reference/api/tools.html" rel="noopener noreferrer"&gt;docs.spring.io/spring-ai/reference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Solon AI docs — &lt;a href="https://solon.noear.org" rel="noopener noreferrer"&gt;solon.noear.org&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>java</category>
      <category>spring</category>
    </item>
    <item>
      <title>Spring AI vs Solon AI - A Practical Comparison for Java Developers</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Thu, 20 Aug 2026 15:29:18 +0000</pubDate>
      <link>https://dev.to/solonjava/spring-ai-vs-solon-ai-a-practical-comparison-for-java-developers-154f</link>
      <guid>https://dev.to/solonjava/spring-ai-vs-solon-ai-a-practical-comparison-for-java-developers-154f</guid>
      <description>&lt;p&gt;If you build AI-powered applications on the JVM today, you will meet two frameworks quickly: &lt;strong&gt;Spring AI&lt;/strong&gt;, the official AI layer of the vast Spring ecosystem, and &lt;strong&gt;Solon AI&lt;/strong&gt;, the AI stack of the Solon project that also embeds into any Java framework — including Spring Boot itself.&lt;/p&gt;

&lt;p&gt;They solve the same problem — &lt;em&gt;connect your Java code to LLMs, tools, RAG pipelines, and agents without locking yourself to one vendor&lt;/em&gt; — but they approach it with very different philosophies. This article compares them feature by feature, with side-by-side code for every major capability. All Solon AI snippets below were verified against the &lt;code&gt;solon-ai&lt;/code&gt; source (v4.x), and the Spring AI snippets follow the current Spring AI 1.0.x reference documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What They Share
&lt;/h2&gt;

&lt;p&gt;The overlap is larger than the marketing suggests. Both frameworks offer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A portable chat model abstraction&lt;/strong&gt; — one interface, many providers (OpenAI, Anthropic, Google Gemini, Ollama, Azure, DeepSeek, DashScope, ...). Write once, swap the backend by configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calling&lt;/strong&gt; via annotated Java methods (&lt;code&gt;@Tool&lt;/code&gt; in Spring AI, &lt;code&gt;@ToolMapping&lt;/code&gt; in Solon AI), with the framework managing the request/execute/return loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output&lt;/strong&gt; — map model responses directly onto Java POJOs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full RAG chain&lt;/strong&gt; — document loaders, splitters, embedding models, vector store adapters (PGVector, Redis, Milvus, Qdrant, Chroma, ...), and metadata filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat memory / conversation persistence&lt;/strong&gt;, with pluggable stores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt; on both sides: consume external MCP servers and expose your own services as MCP servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync and streaming responses&lt;/strong&gt;, plus GraalVM native image support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are Apache 2.0 licensed and production-oriented. The interesting part is where they diverge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference #1: The Ecosystem Contract
&lt;/h2&gt;

&lt;p&gt;This is the deepest difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring AI&lt;/strong&gt; is an AI abstraction layer &lt;em&gt;for the Spring ecosystem&lt;/em&gt;. Its idioms are Spring idioms: Boot starters, auto-configuration, dependency injection, Micrometer observability. It requires Java 17+ (the upcoming 2.0 line targets Java 21+), and in practice it lives inside a Spring Boot application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solon AI&lt;/strong&gt; is framework-neutral. It runs standalone on plain JDK 8 through 26, and it can be embedded into Spring Boot, Quarkus, Vert.x, or jFinal as just another library. If you have a legacy Java 8 service in a bank and want to add LLM features, this distinction decides the question by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference #2: API Style — Advisor Chain vs. Builder + Agent
&lt;/h2&gt;

&lt;p&gt;Spring AI's high-level API is &lt;code&gt;ChatClient&lt;/code&gt;, a fluent builder in the spirit of &lt;code&gt;WebClient&lt;/code&gt;. Cross-cutting behaviors — memory, RAG, tool execution — are composed as an &lt;strong&gt;Advisor chain&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;MessageChatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatMemory&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// memory&lt;/span&gt;
                &lt;span class="nc"&gt;QuestionAnswerAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;// RAG&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What is our refund policy for enterprise contracts?"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advisors are interceptors wrapping the call: they can rewrite the prompt, inject retrieved documents, and post-process the response. It is a clean, extensible pattern — but everything is a ChatClient concern.&lt;/p&gt;

&lt;p&gt;Solon AI instead exposes a ladder of abstractions, from low to high:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ChatModel&lt;/code&gt;&lt;/strong&gt; — the raw, provider-neutral model call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ReActAgent&lt;/code&gt;&lt;/strong&gt; — an agent with role, instructions, session, retries, and reflection loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TeamAgent&lt;/code&gt;&lt;/strong&gt; — multi-agent collaboration with routing protocols.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The equivalent of the snippet above in Solon AI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.openai.com/v1"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;supportBot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SupportBot"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Answer using only the provided knowledge base"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;supportBot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What is our refund policy?"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Difference #3: Tool Calling
&lt;/h2&gt;

&lt;p&gt;The concepts map almost one-to-one; only the annotations and wiring differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring AI&lt;/strong&gt; — annotate a method, register the object, and the auto-registered &lt;code&gt;ToolCallingAdvisor&lt;/code&gt; runs the tool loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WeatherTools&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get current weather for a city"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getWeather&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@ToolParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"City name"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;weatherService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lookup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeatherTools&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Weather in Hangzhou?"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Solon AI&lt;/strong&gt; — same idea with &lt;code&gt;@ToolMapping&lt;/code&gt; and &lt;code&gt;@Param&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WeatherTools&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbsToolProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@ToolMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get current weather for a city"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getWeather&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"City name"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;weatherService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lookup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiUrl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultToolAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeatherTools&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Weather in Hangzhou?"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Solon AI extra worth noting: the &lt;strong&gt;Talent system&lt;/strong&gt;, where entire tool suites (terminal, mail, memory, browser automation) are mounted as swappable capability groups an agent can discover — closer to "skills" than to plain function calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference #4: Structured Output
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spring AI&lt;/strong&gt; converts responses to POJOs at the ChatClient layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ActorFilms&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;movies&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

&lt;span class="nc"&gt;ActorFilms&lt;/span&gt; &lt;span class="n"&gt;films&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"List 3 movies of Tom Hanks"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ActorFilms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Solon AI&lt;/strong&gt; puts the constraint in &lt;code&gt;ChatOptions.outputSchema(...)&lt;/code&gt;. The framework generates a JSON Schema from your POJO and injects it into the instruction in a provider-neutral &lt;code&gt;&amp;lt;output_schema&amp;gt;&lt;/code&gt; block — it does not rely on any vendor's native JSON mode, so the same code works on OpenAI, Ollama, DashScope, or Gemini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;extractor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ResumeExtractor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResumeInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// POJO -&amp;gt; JSON Schema, enforced&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resume"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the response side, &lt;code&gt;AssistantMessage.getJsonContent()&lt;/code&gt; strips markdown fences and &lt;code&gt;toBean(Type)&lt;/code&gt; deserializes back to the same type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference #5: RAG
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spring AI&lt;/strong&gt; treats RAG as advisors. &lt;code&gt;QuestionAnswerAdvisor&lt;/code&gt; implements naive RAG; &lt;code&gt;RetrievalAugmentationAdvisor&lt;/code&gt; composes modular pipelines (query transformers, retrievers, post-processors) inspired by the Modular RAG paper. Filtering uses a portable SQL-like &lt;code&gt;FilterExpression&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Advisor&lt;/span&gt; &lt;span class="n"&gt;rag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RetrievalAugmentationAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;documentRetriever&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VectorStoreDocumentRetriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;similarityThreshold&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Solon AI&lt;/strong&gt; treats RAG as a repository. One abstraction covers embedding, storage, retrieval, filtering, and prompt augmentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ingest&lt;/span&gt;
&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"department"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"support"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieve with a filter expression pushed down to the store&lt;/span&gt;
&lt;span class="nc"&gt;QueryCondition&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QueryCondition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"refund policy"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterExpression&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"department == 'support'"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Let the model drive retrieval itself (agentic RAG)&lt;/span&gt;
&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiUrl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultToolAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RepositoryTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RepositoryTool&lt;/code&gt; turns the repository into a tool, so the model decides when and what to search — agentic RAG out of the box. Spring AI can achieve similar behavior by combining tools with a vector store, but you assemble it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference #6: Memory
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spring AI&lt;/strong&gt;: &lt;code&gt;ChatMemory&lt;/code&gt; stores per-conversation messages (in-memory, JDBC, Cassandra...), surfaced through &lt;code&gt;MessageChatMemoryAdvisor&lt;/code&gt;. Each call must supply &lt;code&gt;ChatMemory.CONVERSATION_ID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solon AI&lt;/strong&gt;: &lt;code&gt;ChatSession&lt;/code&gt; is the message ledger with windowed retrieval (&lt;code&gt;getLatestMessages(windowSize)&lt;/code&gt;), while &lt;code&gt;AgentSession&lt;/code&gt; additionally persists workflow state — including &lt;em&gt;pending human-in-the-loop checkpoints&lt;/em&gt; — with File and Redis backends. The session abstraction covers both chat history and long-running agent execution state in one object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference #7: Agents and Orchestration
&lt;/h2&gt;

&lt;p&gt;This is where the frameworks diverge most in ambition.&lt;/p&gt;

&lt;p&gt;Spring AI's agent story is compositional: advisors + tools + (in recent versions) recursive advisors for self-reflection. Deep agentic workflows in the Spring world often pull in additional projects, e.g. Spring AI Alibaba's graph engine.&lt;/p&gt;

&lt;p&gt;Solon AI ships agent primitives natively: &lt;code&gt;ReActAgent&lt;/code&gt; with reflection and retries, &lt;code&gt;TeamAgent&lt;/code&gt; with built-in collaboration protocols, dynamic skill admission, and &lt;strong&gt;Ai Flow&lt;/strong&gt; — YAML-based flow orchestration for a low-code, Dify-like experience. If your application &lt;em&gt;is&lt;/em&gt; an agent system, Solon AI gives you the vocabulary for it without extra dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference #8: MCP
&lt;/h2&gt;

&lt;p&gt;Both fully support MCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spring AI&lt;/strong&gt;: dedicated Boot starters (&lt;code&gt;spring-ai-starter-mcp-client&lt;/code&gt;, etc.), annotation model (&lt;code&gt;@McpTool&lt;/code&gt;, &lt;code&gt;@McpResource&lt;/code&gt;, &lt;code&gt;@McpPrompt&lt;/code&gt;), sync/async client and server, Stdio/SSE/Streamable-HTTP transports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solon AI&lt;/strong&gt;: &lt;code&gt;@McpServerEndpoint&lt;/code&gt; + &lt;code&gt;@ToolMapping&lt;/code&gt; to expose services; client-side integration folds remote MCP tools into the local tool system; multiple endpoint groups per service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implementation detail: Spring AI builds on the official MCP Java SDK; Solon AI implements the protocol within its own runtime, which keeps the dependency tree small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Difference #9: Runtime Footprint
&lt;/h2&gt;

&lt;p&gt;The Solon project advertises (and its TechEmpower-style results back up) dramatically lower resource usage than comparable Spring Boot stacks: startup in fractions of a second, tens of MB of heap, tiny JARs. For serverless and edge deployments this matters; for a monolith behind an existing observability stack, it matters less. Spring AI compensates with first-class Micrometer integration, Spring Security, and the enormous Spring ecosystem of starters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Spring AI&lt;/th&gt;
&lt;th&gt;Solon AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;Spring Boot native&lt;/td&gt;
&lt;td&gt;Framework-neutral, embeddable anywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java requirement&lt;/td&gt;
&lt;td&gt;17+ (2.0: 21+)&lt;/td&gt;
&lt;td&gt;8 ~ 26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core API&lt;/td&gt;
&lt;td&gt;ChatClient + Advisor chain&lt;/td&gt;
&lt;td&gt;ChatModel + Agent ladder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Tool&lt;/code&gt; + ToolCallback&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ToolMapping&lt;/code&gt; + Talent system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.entity(Class)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;outputSchema(Class)&lt;/code&gt;, provider-neutral&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;td&gt;Advisors, modular pipeline&lt;/td&gt;
&lt;td&gt;Repository abstraction, agentic RAG built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;ChatMemory advisors&lt;/td&gt;
&lt;td&gt;ChatSession + AgentSession (incl. HITL state)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agents&lt;/td&gt;
&lt;td&gt;Compositional (advisors/tools)&lt;/td&gt;
&lt;td&gt;Native ReAct/Team agents + Ai Flow YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;Starters + MCP Java SDK&lt;/td&gt;
&lt;td&gt;Native implementation, multi-endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Footprint&lt;/td&gt;
&lt;td&gt;Typical Spring Boot&lt;/td&gt;
&lt;td&gt;Very small, fast startup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;Micrometer, Actuator&lt;/td&gt;
&lt;td&gt;Solon ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which One Should You Pick?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose Spring AI&lt;/strong&gt; when you already live in Spring Boot, your team's skills are Spring-centric, and you value the ecosystem: auto-configuration, starters for everything, Micrometer observability, and long-term commercial backing from the Spring team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Solon AI&lt;/strong&gt; when you need AI features in a codebase that is not (or cannot be) a modern Spring Boot app: Java 8 legacy services, Quarkus/Vert.x/jFinal stacks, resource-constrained deployments — or when your application is fundamentally an agent system and you want native ReAct, team collaboration, skills, and YAML flow orchestration rather than assembling them from parts.&lt;/p&gt;

&lt;p&gt;And remember the pragmatic option: because Solon AI embeds into Spring Boot, the choice is not always either/or — some teams use Spring Boot for the web tier and Solon AI for the agent core, taking the best of both ecosystems.&lt;/p&gt;

&lt;p&gt;Both frameworks move fast, both are genuinely production-capable, and the competition between them is exactly what the Java AI ecosystem needs right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Spring AI reference docs: &lt;a href="https://docs.spring.io/spring-ai/reference/" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-ai/reference/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Spring AI project page: &lt;a href="https://spring.io/projects/spring-ai" rel="noopener noreferrer"&gt;https://spring.io/projects/spring-ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Solon AI repository: &lt;a href="https://github.com/opensolon/solon-ai" rel="noopener noreferrer"&gt;https://github.com/opensolon/solon-ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Solon AI docs: &lt;a href="https://solon.noear.org/article/learn-solon-ai" rel="noopener noreferrer"&gt;https://solon.noear.org/article/learn-solon-ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model Context Protocol: &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>spring</category>
      <category>solon</category>
      <category>ai</category>
    </item>
    <item>
      <title>Give Your Java Agents a Memory - Session Management with Solon AI</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Thu, 20 Aug 2026 15:17:07 +0000</pubDate>
      <link>https://dev.to/solonjava/give-your-java-agents-a-memory-session-management-with-solon-ai-2nja</link>
      <guid>https://dev.to/solonjava/give-your-java-agents-a-memory-session-management-with-solon-ai-2nja</guid>
      <description>&lt;p&gt;Most LLM demos are amnesiacs. The user says "my name is noear and I like blue" in turn one, asks "what's my name?" in turn two, and the model shrugs - because every HTTP call to the chat API is stateless, and nobody fed the history back in. In production this is not a cosmetic issue: a support agent that forgets the ticket the customer opened 30 seconds ago is worse than no agent at all.&lt;/p&gt;

&lt;p&gt;Solon AI (v4.0.5) treats conversation state as a first-class, pluggable construct. In this article we build a multi-turn customer support agent whose memory survives process restarts and horizontal scaling, using only the framework's session abstractions - no hand-rolled history tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with hand-rolled memory
&lt;/h2&gt;

&lt;p&gt;The naive fix is to append every message to a &lt;code&gt;List&amp;lt;ChatMessage&amp;gt;&lt;/code&gt; in your own code and resend it with each request. That works until it doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded growth&lt;/strong&gt; - a 200-turn ticket means 400 messages re-sent (and re-billed as tokens) on every call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System prompt pollution&lt;/strong&gt; - if you persist everything, stale system prompts pile up inside the history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No place for workflow state&lt;/strong&gt; - agents don't just remember text; they remember &lt;em&gt;where they are&lt;/em&gt; in a plan, which steps finished, what a human approved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restart amnesia&lt;/strong&gt; - in-memory lists die with the JVM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solon AI answers each of these with a dedicated layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  ChatSession: the message ledger
&lt;/h2&gt;

&lt;p&gt;At the core sits &lt;code&gt;ChatSession&lt;/code&gt; (&lt;code&gt;org.noear.solon.ai.chat&lt;/code&gt;, since 3.1) - a deliberately small interface that models the conversation as an append-only message sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ChatSession&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getSessionId&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getMessages&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getLatestMessages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;windowSize&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;removeLatestMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;windowSize&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;addMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;addMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userMessage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// convenience: user role&lt;/span&gt;

    &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                  &lt;span class="c1"&gt;// transient, never persisted&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details are worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getLatestMessages(windowSize)&lt;/code&gt;&lt;/strong&gt; is the windowing primitive. The framework itself uses it to inject only the last N turns into the prompt - the unbounded-growth problem is solved at the retrieval side, not by deleting history. The full ledger stays available for audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;attrs()&lt;/code&gt;&lt;/strong&gt; is explicitly documented as &lt;em&gt;not&lt;/em&gt; needing persistence - a place for per-request scratch data that must never leak into your storage layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AgentSession: memory plus workflow state
&lt;/h2&gt;

&lt;p&gt;Agents need more than a transcript. &lt;code&gt;AgentSession&lt;/code&gt; (since 3.8.1) extends &lt;code&gt;ChatSession&lt;/code&gt; with the state of the agent's execution flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;AgentSession&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ChatSession&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;updateSnapshot&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// sync execution snapshot&lt;/span&gt;
    &lt;span class="nc"&gt;FlowContext&lt;/span&gt; &lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// live flow context&lt;/span&gt;

    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// suspend / resume&lt;/span&gt;
    &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isPending&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getPendingReason&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pending(...)&lt;/code&gt; family is how human-in-the-loop agents park themselves mid-plan: suspend with a reason ("waiting for expense approval"), serialize the whole session, and resume the exact step when the human answers. Because the snapshot lives &lt;em&gt;inside&lt;/em&gt; the session object, one storage backend covers both transcript and workflow state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attaching a session to an agent
&lt;/h2&gt;

&lt;p&gt;Every agent request carries its session explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...;&lt;/span&gt;

&lt;span class="nc"&gt;SimpleAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SimpleAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SupportAgent"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A customer support assistant"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Track the customer's issue across the whole conversation."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionWindowSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// inject last 10 messages as history&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;AgentSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryAgentSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer-8837"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Turn 1&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"My order #5521 arrived broken, I want a replacement."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Turn 2 - minutes later, same session: the agent already knows the order number&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"It was the blue ceramic mug, by the way."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the framework does per call (from the &lt;code&gt;SimpleAgent&lt;/code&gt; source):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Builds the agent prompt, pulling &lt;code&gt;session.getLatestMessages(config.getSessionWindowSize())&lt;/code&gt; as history (default window: 5).&lt;/li&gt;
&lt;li&gt;Stamps &lt;code&gt;__sessionId&lt;/code&gt; into both the prompt attributes and the tool context - so custom tools you write can correlate database writes with the conversation.&lt;/li&gt;
&lt;li&gt;Appends the assistant's reply with &lt;code&gt;session.addMessage(...)&lt;/code&gt; and calls &lt;code&gt;updateSnapshot()&lt;/code&gt; - your code never mutates history manually.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pluggable backends
&lt;/h2&gt;

&lt;p&gt;Sessions are an interface, and three backends ship in the box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Messages&lt;/th&gt;
&lt;th&gt;Snapshot&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InMemoryAgentSession&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JVM heap&lt;/td&gt;
&lt;td&gt;JVM heap&lt;/td&gt;
&lt;td&gt;tests, single-node demos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FileAgentSession&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NDJSON append log&lt;/td&gt;
&lt;td&gt;JSON file&lt;/td&gt;
&lt;td&gt;single instance, zero infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RedisAgentSession&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Redis list (&lt;code&gt;&amp;lt;id&amp;gt;:messages&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Redis key (&lt;code&gt;&amp;lt;id&amp;gt;:snapshot&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;production, multi-instance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;FileAgentSession&lt;/code&gt; behaves like a proper write-ahead log. The official test suite demonstrates the property that matters most - restart recovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileAgentSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileAgentSession&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tempDir&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                   &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofAssistant&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hi, how can I help?"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user_name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"noear"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateSnapshot&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// simulate a process restart: new instance, same directory&lt;/span&gt;
&lt;span class="nc"&gt;FileAgentSession&lt;/span&gt; &lt;span class="n"&gt;recovered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileAgentSession&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tempDir&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;recovered&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessages&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                 &lt;span class="c1"&gt;// 2 - transcript survived&lt;/span&gt;
&lt;span class="n"&gt;recovered&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user_name"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// "noear" - snapshot survived&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the subtlety verified by the same tests: &lt;strong&gt;system messages are filtered out of the NDJSON log&lt;/strong&gt;. Persisted history contains only the real conversation (user/assistant/tool), so reloading never stacks stale system prompts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RedisAgentSession&lt;/code&gt; adds an in-memory cache layer with per-session locking, so hot conversations don't pay a network round trip per message, while the canonical state lives in Redis - which is what you want when the support team's traffic lands on a load balancer and turn two may hit a different pod than turn one.&lt;/p&gt;

&lt;h2&gt;
  
  
  One provider to route them all
&lt;/h2&gt;

&lt;p&gt;The last piece is &lt;code&gt;AgentSessionProvider&lt;/code&gt; - a one-method factory the framework uses to resolve sessions by business ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AgentSessionProvider&lt;/span&gt; &lt;span class="nf"&gt;redisSession&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RedisClient&lt;/span&gt; &lt;span class="n"&gt;redisClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AgentSession&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;computeIfAbsent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisAgentSession&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redisClient&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract is lazy loading: return the existing session if there is one (keeping context continuous), create one on demand otherwise. Inject it wherever agents are used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt; &lt;span class="nc"&gt;AgentSessionProvider&lt;/span&gt; &lt;span class="n"&gt;sessionProvider&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nd"&gt;@Inject&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;supportAgent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;AgentSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sessionProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;supportAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the session ID is your business key ("customer-8837"), memory becomes addressable: the same customer chatting from the app and from email can be routed to the same session, and your data retention tooling can age sessions out by exactly the keys it already knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows, not truncation
&lt;/h2&gt;

&lt;p&gt;A common misconception is that windowing means deleting history. In Solon AI the two are separate concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getLatestMessages(n)&lt;/code&gt; - what the model &lt;em&gt;sees&lt;/em&gt; (token cost control)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getMessages()&lt;/code&gt; - what your system &lt;em&gt;knows&lt;/em&gt; (audit, analytics, compliance)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a support platform can show the customer their full transcript in the UI, feed the agent only the last 10 messages for cost, and keep everything in NDJSON or Redis for the retention policy - one session object, three views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Statelessness is the LLM's constraint, not yours. Solon AI's session layer turns "agent with memory" from a hand-rolled liability (growing lists, lost state on deploy, no audit trail) into a configuration decision: pick a backend, set a window, inject a provider. The transcript, the workflow snapshot and the human-in-the-loop suspension point all ride in the same persistent object - and swapping the demo &lt;code&gt;InMemoryAgentSession&lt;/code&gt; for the production &lt;code&gt;RedisAgentSession&lt;/code&gt; is a one-line change.&lt;/p&gt;

&lt;p&gt;If you want to dig into the agent framework, chat models, RAG or tool calling, the docs live at &lt;a href="https://solon.noear.org" rel="noopener noreferrer"&gt;solon.noear.org&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>From Messy Text to Reliable Java Objects - Structured Output Extraction with Solon AI</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Thu, 20 Aug 2026 03:50:41 +0000</pubDate>
      <link>https://dev.to/solonjava/from-messy-text-to-reliable-java-objects-structured-output-extraction-with-solon-ai-5pl</link>
      <guid>https://dev.to/solonjava/from-messy-text-to-reliable-java-objects-structured-output-extraction-with-solon-ai-5pl</guid>
      <description>&lt;p&gt;Every enterprise has a pile of documents that machines cannot read yet: supplier invoices arriving as free-text emails, résumés in a dozen formats, support tickets, contract clauses. The downstream systems - ERP, ATS, CRM - all want clean, typed data. Bridging that gap has traditionally meant brittle regex farms or template matchers that break on the first layout change.&lt;/p&gt;

&lt;p&gt;Large language models are obviously good at reading this text. The unsolved part is the &lt;em&gt;output contract&lt;/em&gt;. If you just ask a model "return JSON", you get JSON decorated with markdown fences, occasional prose preambles, missing fields, or a friendly sentence explaining why it could not comply. That is fine for a demo and fatal for a database insert.&lt;/p&gt;

&lt;p&gt;This article shows how Solon AI (v4.0.5) treats structured extraction as a first-class capability: you declare the target as a plain Java type, the framework generates the JSON Schema, injects it into the conversation, extracts the JSON from whatever the model actually returned, and deserializes it back into your bean - with retries, sessions and agents when you need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "please return JSON"
&lt;/h2&gt;

&lt;p&gt;The common workaround is prompt engineering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract the invoice fields and return ONLY valid JSON matching: {invoice_no, total, currency, ...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three failure modes in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decoration drift.&lt;/strong&gt; Models wrap output in

```json fences, add "Here is the JSON:", or both. Your parser dies on character one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema amnesia.&lt;/strong&gt; Long prompts, many fields - the model forgets &lt;code&gt;tax_rate&lt;/code&gt; or invents &lt;code&gt;taxRate&lt;/code&gt;. Nothing catches it before the &lt;code&gt;INSERT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider lock-in.&lt;/strong&gt; Some vendors offer native JSON mode or &lt;code&gt;response_format&lt;/code&gt; with strict schemas. Your extraction code then only works with that one vendor, and the migration cost multiplies across every extraction job you own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Solon AI addresses all three with a type-driven pipeline that runs on &lt;em&gt;any&lt;/em&gt; ChatModel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Declare the target type
&lt;/h2&gt;

&lt;p&gt;Extraction targets are ordinary Java beans. Fields can carry &lt;code&gt;@Param&lt;/code&gt; annotations to enrich the generated schema with descriptions, required flags and defaults:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
java
public class InvoiceInfo {
    @Param(description = "Invoice number, e.g. INV-2025-0311", required = true)
    public String invoiceNo;

    @Param(description = "Vendor legal name", required = true)
    public String vendor;

    @Param(description = "Total amount in minor currency units", required = true)
    public Long totalAmount;

    @Param(description = "ISO-4217 currency code")
    public String currency;

    @Param(description = "Due date in yyyy-MM-dd")
    public String dueDate;

    @Param(description = "Extracted line items")
    public LineItem[] items;

    public static class LineItem {
        @Param(description = "Item description")
        public String description;

        @Param(description = "Quantity", required = true, defaultValue = "1")
        public Integer quantity;

        @Param(description = "Unit price in minor units")
        public Long unitPrice;
    }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Why descriptions matter: they end up inside the JSON Schema that the model reads. A field named &lt;code&gt;totalAmount&lt;/code&gt; is ambiguous (gross? net? VAT included? major or minor units?). The schema text is the cheapest place to remove that ambiguity - cheaper than debugging wrong numbers in your ledger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Attach the schema and call
&lt;/h2&gt;

&lt;p&gt;With just the core chat API, the schema rides on the request options:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
java
ChatModel chatModel = ChatModel.of("http://localhost:11434/api/chat")
        .apiKey(System.getenv("LLM_API_KEY"))
        .model("qwen3:14b")
        .build();

ChatResponse resp = chatModel
        .prompt(invoiceEmailText)
        .options(o -&amp;gt; o.outputSchema(InvoiceInfo.class))
        .call();

InvoiceInfo invoice = resp.getMessage().toBean(InvoiceInfo.class);

System.out.println(invoice.invoiceNo + " -&amp;gt; " + invoice.totalAmount);


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That is the whole extraction call. Three things happened under the hood worth understanding, because they are the difference between this and prompt-begging.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the schema reaches the model
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;outputSchema&lt;/code&gt; is set, the request pipeline hands the schema string to the active chat &lt;em&gt;dialect&lt;/em&gt;, which appends it to the instruction block:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
json
&amp;lt;output_schema&amp;gt;
{"type":"object","properties":{"invoiceNo":{"type":"string","description":"Invoice number, e.g. INV-2025-0311"}, ...},"required":["invoiceNo","vendor","totalAmount", ...]}
&amp;lt;/output_schema&amp;gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The schema itself is generated from the Java type by the framework (snack4's JsonSchema generator walks the &lt;code&gt;TypeEggg&lt;/code&gt; metadata of your bean, consuming the &lt;code&gt;@Param&lt;/code&gt; annotations along the way). Primitive wrappers, strings, enums and simple types don't even need a schema - only structured objects do, so your prompt stays minimal when a type degenerates to a string.&lt;/p&gt;

&lt;p&gt;Because the injection happens through the &lt;code&gt;ChatDialect&lt;/code&gt; extension point, every provider benefits - Ollama, OpenAI, DashScope, Gemini, Anthropic - and a vendor with a native strict JSON mode could override the hook. Your extraction code never branches on provider capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the answer comes back
&lt;/h3&gt;

&lt;p&gt;Models will still decorate. &lt;code&gt;AssistantMessage.getJsonContent()&lt;/code&gt; strips the noise: it locates the first &lt;code&gt;{&lt;/code&gt; or &lt;code&gt;[&lt;/code&gt; and the matching last closing brace, so markdown fences, "Here is the JSON:" preambles and trailing commentary are all tolerated. &lt;code&gt;toBean()&lt;/code&gt; then deserializes the extracted JSON into the target type. You can also call &lt;code&gt;getResultContent()&lt;/code&gt; to get thinking-stripped raw text if you want to run your own validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you do NOT get for free
&lt;/h3&gt;

&lt;p&gt;Honesty section: a schema improves field discipline dramatically, but it cannot make a small model do reliable arithmetic. For invoice totals, compute from line items in Java; use extraction only to &lt;em&gt;read&lt;/em&gt;, not to &lt;em&gt;sum&lt;/em&gt;. Validation belongs in your code:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
java
if (invoice.totalAmount == null || invoice.items == null || invoice.items.length == 0) {
    throw new ExtractionException("invoice fields missing: " + invoice.invoiceNo);
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Step 3: Promote to an extraction agent
&lt;/h2&gt;

&lt;p&gt;Single calls are fine for one document. A production pipeline processing hundreds of résumés per day wants retries, low temperature, and a place to collect results. This is where &lt;code&gt;SimpleAgent&lt;/code&gt; earns its name:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
java
SimpleAgent resumeAgent = SimpleAgent.of(chatModel)
        .name("ResumeExtractor")
        .role("HR assistant specialized in résumé parsing")
        .instruction("Extract the key facts from the candidate text provided by the user")
        .outputSchema(ResumeInfo.class)
        .outputKey("extracted_resume")
        .retryConfig(3, 2000L)
        .modelOptions(o -&amp;gt; o.temperature(0.1F))
        .build();

public static class ResumeInfo {
    public String name;
    public Integer age;
    public String email;
    public String[] capabilities;
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Running it against a batch:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
java
AgentSession session = InMemoryAgentSession.of("resume-batch-01");

for (String rawResume : inboundResumes) {
    AssistantMessage message = resumeAgent
            .prompt(Prompt.of(rawResume))
            .session(session)
            .call()
            .getMessage();

    // Path A: typed bean directly
    ResumeInfo info = message.toBean(ResumeInfo.class);

    // Path B: same result, stored in the session context under outputKey
    String extractedJson = (String) session.getContext().get("extracted_resume");

    atsService.upsert(info);
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The knobs that matter for extraction jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;outputSchema(ResumeInfo.class)&lt;/code&gt;&lt;/strong&gt; - same type-driven schema as the raw ChatModel path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;outputKey("extracted_resume")&lt;/code&gt;&lt;/strong&gt; - the agent also writes the extracted JSON into the session context, so downstream steps (an enrichment agent, a human reviewer, an export job) can pick it up without re-parsing the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retryConfig(3, 2000L)&lt;/code&gt;&lt;/strong&gt; - up to 3 retries with a 2s delay. Schema violations and unparseable output are exactly the transient failures retries absorb.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;modelOptions(o -&amp;gt; o.temperature(0.1F))&lt;/code&gt;&lt;/strong&gt; - extraction is reading, not writing poetry. Low temperature measurably reduces format drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same builder surface exists on &lt;code&gt;ReActAgent&lt;/code&gt; for extraction jobs that need tools first - say, the agent must call a country-code lookup service before it can normalize a phone number, then emit the final structured result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the pieces live
&lt;/h2&gt;

&lt;p&gt;For reference, the moving parts in the Solon AI modules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;Entry point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schema generation from Java types&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ToolSchemaUtil.buildOutputSchema(Type)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema injection per provider&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon-ai-core&lt;/code&gt; dialects&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ChatDialect.prepareOutputSchemaInstruction(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON stripping + deserialization&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AssistantMessage.getJsonContent()&lt;/code&gt; / &lt;code&gt;toBean(Type)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent pipeline (retry, outputKey)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SimpleAgent&lt;/code&gt; / &lt;code&gt;ReActAgent&lt;/code&gt; builders&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dependencies for a minimal extraction service:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
xml
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.noear&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;solon-ai-core&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.noear&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;solon-ai-dialect-ollama&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- or: solon-ai-dialect-openai / dashscope / gemini / anthropic --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.noear&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;solon-ai-agent&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Beyond invoices and résumés
&lt;/h2&gt;

&lt;p&gt;The same pattern generalizes to any text-to-record workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Support tickets → structured incident records&lt;/strong&gt; (severity, product area, entitlement) feeding your ticketing API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract clauses → obligation registers&lt;/strong&gt; with counterparty, deadline, penalty fields for a compliance dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensor log excerpts → fault codes&lt;/strong&gt; for maintenance planning, where the "text" is semi-structured machine output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent-to-agent handoff&lt;/strong&gt; - one agent's structured output is the next agent's typed input, with &lt;code&gt;outputKey&lt;/code&gt; as the contract slot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When your extraction job outgrows a single call, the schema composes with the rest of the Solon AI stack: wrap it in a &lt;code&gt;TeamAgent&lt;/code&gt; workflow with a human-in-the-loop checkpoint for low-confidence records, or let a &lt;code&gt;Loop&lt;/code&gt;-based validator re-prompt with the validation error until the bean passes - the schema gives the loop a concrete failure signal to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Structured extraction fails in production for boring reasons: fences around the JSON, renamed fields, vendor-specific JSON modes. Solon AI's answer is to make the Java type the single source of truth - generate the schema from it, transport it in a provider-neutral way, strip whatever decoration the model adds, and deserialize back into the same type that generated the contract. Add retries and low temperature at the agent layer, and the gap between "the model can read it" and "the database can accept it" closes to a few lines of code.&lt;/p&gt;

&lt;p&gt;The framework is open source, Java 8 to 26, and the docs live at &lt;a href="https://solon.noear.org" rel="noopener noreferrer"&gt;solon.noear.org&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Integrating External Tool Ecosystems with Solon AI's MCP Protocol</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:32:08 +0000</pubDate>
      <link>https://dev.to/solonjava/integrating-external-tool-ecosystems-with-solon-ais-mcp-protocol-3bha</link>
      <guid>https://dev.to/solonjava/integrating-external-tool-ecosystems-with-solon-ais-mcp-protocol-3bha</guid>
      <description>&lt;p&gt;Large Language Models (LLMs) are only as powerful as the tools they can access. While internal tools are easy to wire directly into a Java application, the real challenge begins when you need your AI agents to reach &lt;strong&gt;out&lt;/strong&gt; — to call REST APIs, query databases, invoke third-party services, or even communicate with other AI systems.&lt;/p&gt;

&lt;p&gt;Hardcoding every external dependency creates a fragile architecture. Every new tool requires code changes, new dependencies, and new configurations. This is where the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;In this article, we explore how &lt;strong&gt;Solon AI v4.0.5&lt;/strong&gt; implements MCP to solve one of the hardest problems in AI engineering: &lt;strong&gt;How do we let agents access external tool ecosystems in a standardized, reusable, and secure way?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Tool Sprawl in AI Applications
&lt;/h2&gt;

&lt;p&gt;Imagine you're building a financial analysis agent. Initially, it needs access to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stock price APIs&lt;/li&gt;
&lt;li&gt;Company financial report databases&lt;/li&gt;
&lt;li&gt;Real-time news feeds&lt;/li&gt;
&lt;li&gt;Regulatory filing systems&lt;/li&gt;
&lt;li&gt;Risk scoring models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you hardcode all of these, every new tool requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;New Maven dependencies&lt;/li&gt;
&lt;li&gt;New service classes&lt;/li&gt;
&lt;li&gt;New tool registration code&lt;/li&gt;
&lt;li&gt;New configuration properties&lt;/li&gt;
&lt;li&gt;New error handling logic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And if you want to &lt;strong&gt;share&lt;/strong&gt; these tools across different applications or with other teams? You'd have to copy-paste entire codebases or rebuild everything from scratch.&lt;/p&gt;

&lt;p&gt;This is exactly the problem MCP solves. By adopting an open protocol, tools become &lt;strong&gt;composable, shareable, and reusable&lt;/strong&gt; — just like Linux executables or npm packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MCP?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; is an open standard for connecting AI applications to external data sources and tools. Think of it as &lt;strong&gt;USB-C for AI agents&lt;/strong&gt; — a universal connector that lets any client talk to any server using a common language.&lt;/p&gt;

&lt;p&gt;The protocol defines three core primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: Functions the agent can call (like &lt;code&gt;get_weather(city)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt;: Data the agent can read (like &lt;code&gt;weather://forecast/{city}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt;: Reusable template instructions (like &lt;code&gt;summarize_report&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally developed by Anthropic and now maintained by the MCP Open Source Project, the protocol has gained massive adoption. As of 2025, over 1,200 MCP servers exist across 30+ categories — from file systems and databases to email, calendars, and code repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solon AI's MCP Integration
&lt;/h2&gt;

&lt;p&gt;Solon AI v4.0.5 adds deep MCP support through the &lt;code&gt;solon-ai-mcp&lt;/code&gt; module. Let's look at how it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server Side: Exposing Tools via MCP
&lt;/h3&gt;

&lt;p&gt;To expose a Java service as an MCP server, you only need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the &lt;code&gt;solon-ai-mcp&lt;/code&gt; dependency&lt;/li&gt;
&lt;li&gt;Annotate your tool class with &lt;code&gt;@McpServerEndpoint&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;@ToolMapping&lt;/code&gt; for tools, &lt;code&gt;@ResourceMapping&lt;/code&gt; for resources&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a complete example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@McpServerEndpoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;McpChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STREAMABLE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mcpEndpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/mcp/weather"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WeatherTool&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@ToolMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get current weather for a city"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getWeather&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Call external weather API&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetchWeatherData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ResourceMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"weather://forecast/{city}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Get weather forecast"&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getForecast&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;buildForecastJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Solon AI automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registers the tools and resources with the MCP server&lt;/li&gt;
&lt;li&gt;Handles JSON-RPC message serialization/deserialization&lt;/li&gt;
&lt;li&gt;Manages the transport layer (HTTP, SSE, or Stdio)&lt;/li&gt;
&lt;li&gt;Supports the &lt;code&gt;notifications/tools/list_changed&lt;/code&gt; event for dynamic updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Client Side: Using Remote Tools as Local Beans
&lt;/h3&gt;

&lt;p&gt;The real power shines on the client side. Once a tool is exposed via MCP, any Solon AI agent can use it as if it were a local bean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyAppConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;McpTalentClient&lt;/span&gt; &lt;span class="nf"&gt;weatherClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;McpClientProvider&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;McpTalentClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;McpTalentClient&lt;/span&gt; &lt;span class="n"&gt;weatherTool&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"You are a travel assistant..."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultToolAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weatherTool&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Inject MCP tool&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;McpTalentClient&lt;/code&gt; acts as a &lt;strong&gt;bridge&lt;/strong&gt; between your agent and the remote MCP server. When the agent calls &lt;code&gt;getWeather()&lt;/code&gt;, the call is routed over the network to the server, executed, and the result is returned — all transparently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transport Options
&lt;/h3&gt;

&lt;p&gt;Solon AI supports four transport channels via &lt;code&gt;McpChannel&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STDIO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local CLI tools, subprocess invocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long-running server connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STREAMABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Modern HTTP-based (recommended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STREAMABLE_STATELESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stateless HTTP, no session management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For production systems, &lt;code&gt;STREAMABLE&lt;/code&gt; is recommended because it provides both HTTP/2 multiplexing and session management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use Case: Multi-Source Financial Analysis
&lt;/h2&gt;

&lt;p&gt;Let's build a practical example. Imagine a compliance agent that needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query a company database for financial records&lt;/li&gt;
&lt;li&gt;Call a risk scoring API&lt;/li&gt;
&lt;li&gt;Fetch regulatory updates from an external feed&lt;/li&gt;
&lt;li&gt;Generate a compliance report&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With MCP, each data source becomes a separate &lt;strong&gt;MCP server&lt;/strong&gt; that can be developed, deployed, and updated independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.yml&lt;/span&gt;
&lt;span class="na"&gt;solon&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;financial-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streamable&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8081/mcp&lt;/span&gt;
        &lt;span class="na"&gt;risk-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streamable&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8082/mcp&lt;/span&gt;
        &lt;span class="na"&gt;regulatory-feed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streamable&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8083/mcp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compliance agent doesn't know (or care) which server provides which tool. It just declares what it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ToolMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Query company financials"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;FinancialData&lt;/span&gt; &lt;span class="nf"&gt;queryFinancials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nd"&gt;@ToolMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Calculate risk score"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;RiskScore&lt;/span&gt; &lt;span class="nf"&gt;calculateRisk&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt; &lt;span class="nc"&gt;FinancialData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nd"&gt;@ToolMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Fetch regulatory updates"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getRegulatoryUpdates&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the agent runs, it automatically discovers and calls the appropriate tools. If you need to update the risk API, you only change the server code — the agent remains untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Stateful vs Stateless Servers
&lt;/h2&gt;

&lt;p&gt;Solon AI v4.0.5 supports both &lt;strong&gt;stateful&lt;/strong&gt; and &lt;strong&gt;stateless&lt;/strong&gt; MCP servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stateful servers&lt;/strong&gt; (&lt;code&gt;McpServerHost&lt;/code&gt;): Maintain connection state, support streaming, ideal for long-running services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless servers&lt;/strong&gt; (&lt;code&gt;StatelessMcpServerHost&lt;/code&gt;): No connection state, simpler deployment, better for microservice architectures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most enterprise applications, stateless servers are preferred because they're easier to scale horizontally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;p&gt;When exposing tools via MCP, security is paramount:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: Use HTTP headers or JWT tokens to authenticate clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization&lt;/strong&gt;: Implement role-based access control at the tool level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input Validation&lt;/strong&gt;: Always validate tool parameters to prevent injection attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate Limiting&lt;/strong&gt;: Protect backend services from abuse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit Logging&lt;/strong&gt;: Log all tool calls for compliance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Solon AI's &lt;code&gt;McpPlugin&lt;/code&gt; supports custom &lt;code&gt;ServerTransportSecurityValidator&lt;/code&gt; implementations for advanced security scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;MCP is not just a technical solution — it's a &lt;strong&gt;cultural shift&lt;/strong&gt; in how we build AI applications. Instead of every developer reinventing the wheel for each new tool, we now have a shared ecosystem where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool creators&lt;/strong&gt; can build once and deploy everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent developers&lt;/strong&gt; can discover and compose tools without knowing implementation details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise IT&lt;/strong&gt; can manage tool access centrally with consistent security policies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the MCP ecosystem grows, we'll see more specialized servers emerge — from code review tools to database query optimizers to legal document reviewers. The possibilities are limited only by our imagination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;To add MCP support to your Solon AI project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-ai-mcp&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.0.5&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then follow the patterns shown in this article. The full source code examples are available in the &lt;a href="https://github.com/solonlab/solon-ai" rel="noopener noreferrer"&gt;Solon AI GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;MCP is solving one of the most fundamental challenges in AI engineering: &lt;strong&gt;How do we make tools composable and reusable?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By implementing MCP support, Solon AI gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standardized integration&lt;/strong&gt; — connect to any MCP server using a common protocol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool reuse&lt;/strong&gt; — build once, deploy everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt; — enterprise-grade authentication and authorization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt; — support for multiple transport channels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The future of AI is not about building bigger models — it's about connecting smarter tools. And MCP is the protocol that makes that possible.&lt;/p&gt;

&lt;p&gt;Ready to explore? Check out the &lt;a href="https://solon.noear.org" rel="noopener noreferrer"&gt;Solon AI documentation&lt;/a&gt; and join the &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;MCP community&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>The Solon AI Talent System: Solving Real Business Problems with Composable Agent Capabilities</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Wed, 19 Aug 2026 10:13:44 +0000</pubDate>
      <link>https://dev.to/solonjava/the-solon-ai-talent-system-solving-real-business-problems-with-composable-agent-capabilities-3f6</link>
      <guid>https://dev.to/solonjava/the-solon-ai-talent-system-solving-real-business-problems-with-composable-agent-capabilities-3f6</guid>
      <description>&lt;p&gt;Every LLM agent library gives you tools. Few give you a coherent system for organizing those tools into reusable, context-aware skill clusters that can adapt to the business problem at hand — not the other way around. Solon AI's Talent system is that organization layer.&lt;/p&gt;

&lt;p&gt;In this post I'll walk through what a Talent actually is, how it differs from a plain tool, and how Solon ships over thirty built-in Talents covering everything from code search and memory management to enterprise messaging and file operations. Then I'll show how to compose them to solve real business problems: sending a report email with attachments, pushing a deployment notification to your team chat, and running a self-improving agent that learns from past conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Talent actually is
&lt;/h2&gt;

&lt;p&gt;In Solon AI, a &lt;code&gt;Talent&lt;/code&gt; is a bundle of three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; — the &lt;code&gt;FunctionTool&lt;/code&gt; methods the LLM can call (annotated with &lt;code&gt;@ToolMapping&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instructions&lt;/strong&gt; — a dynamic prompt fragment injected into the system message when the Talent is active, telling the model &lt;em&gt;how&lt;/em&gt; to use those tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata&lt;/strong&gt; — name, description, and an enable/disable flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key difference from a plain tool is &lt;strong&gt;context awareness&lt;/strong&gt;. A &lt;code&gt;Talent&lt;/code&gt; can decide whether it's relevant to the current conversation via &lt;code&gt;isSupported(Prompt)&lt;/code&gt;, initialize state when attached via &lt;code&gt;onAttach(Prompt)&lt;/code&gt;, and even swap its tool set dynamically based on what the user is asking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyTalent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbsTalent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isSupported&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Prompt&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserContent&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"代码"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getInstruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Prompt&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"## 代码搜索指南\n你可以通过 codesearch 工具查找 API 文档和示例代码...\n"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@ToolMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my_tool"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Do something useful"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;myTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"param"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AbsTalent&lt;/code&gt; (the convenience base class) handles the boilerplate: it reflects over &lt;code&gt;@ToolMapping&lt;/code&gt; methods to auto-register tools, manages the enabled/disabled lifecycle, and exposes &lt;code&gt;getToolMap()&lt;/code&gt; and &lt;code&gt;getToolAry()&lt;/code&gt; for introspection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The built-in Talent ecosystem
&lt;/h2&gt;

&lt;p&gt;Solon AI ships a rich set of Talents across dedicated Maven modules. Here's a survey of the ones most relevant to business applications:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Talent&lt;/th&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TerminalTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-cli&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;File read/write/ls/grep/glob + bash execution with sandbox support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MemoryTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long-term memory: extract, recall, search, consolidate, prune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WebsearchTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Real-time web search via Exa MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CodeSearchTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code repo search via Exa MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MailTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-mail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send emails with HTML body and file attachments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DingTalkTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-social&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Push messages to DingTalk (with HMAC signature)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FeishuTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-social&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Push Lark/Feishu cards (with signature)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WeComTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-social&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Push messages to WeCom / Enterprise WeChat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Text2SqlTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-text2sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Natural language to SQL with dialect support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PdfTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-pdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PDF reading and extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RedisTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Redis read/write operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LspTalent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-ai-talent-lsp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Language Server Protocol integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All Talents are opt-in. You挂 them onto a &lt;code&gt;ReActAgent&lt;/code&gt; or &lt;code&gt;HarnessEngine&lt;/code&gt; explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultTalentAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TerminalTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mountManager&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;solutionProvider&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MailTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workDir&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pass&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DingTalkTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webhookUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each Talent's &lt;code&gt;isSupported()&lt;/code&gt; method is evaluated at call time. If the current prompt matches, the Talent activates, its &lt;code&gt;getInstruction()&lt;/code&gt; is injected into the system message, and its tools become available to the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business scenario 1: automated report delivery
&lt;/h2&gt;

&lt;p&gt;A common enterprise pattern: generate a report, attach it, and email it to stakeholders. With Solon AI, this is two Talents working together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MailTalent&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MailTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"/path/to/workdir"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"smtp.company.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;465&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"agent@company.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"smtp-password"&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;DingTalkTalent&lt;/span&gt; &lt;span class="n"&gt;dingtalk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DingTalkTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"https://oapi.dingtalk.com/robot/send?access_token=xxx"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"SECxxxxxxxx"&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultTalentAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dingtalk&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user says &lt;em&gt;"Generate the monthly sales report and send it to the team,"&lt;/em&gt; the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Uses &lt;code&gt;TerminalTalent&lt;/code&gt; to generate the report file in the work directory.&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;send_email&lt;/code&gt; on &lt;code&gt;MailTalent&lt;/code&gt;, which auto-detects the attachment path, reads the file bytes, and sends via SMTPS (port 465, TLS). The &lt;code&gt;MailTalent&lt;/code&gt; resolves attachment paths relative to the work directory and validates them against the root path to prevent path traversal.&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;send_dingtalk&lt;/code&gt; on &lt;code&gt;DingTalkTalent&lt;/code&gt;, which packages the summary as a markdown message, computes the HmacSHA256 signature, and pushes to the webhook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both operations are synchronous from the agent's perspective. &lt;code&gt;MailTalent&lt;/code&gt; uses a connection pool (&lt;code&gt;coreSize=2&lt;/code&gt;) for SMTP sessions, so high-frequency report runs don't open a new TCP connection each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business scenario 2: multi-platform team notification
&lt;/h2&gt;

&lt;p&gt;In a multi-office company, different teams use different platforms — DingTalk in China, Feishu in some subsidiaries, WeCom in others. Rather than writing three separate notification integrations, you挂 all three Talents and let the model pick the right one based on context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultTalentAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DingTalkTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dingtalkUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dingtalkSecret&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FeishuTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feishuUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feishuSecret&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WeComTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wecomUrl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each Talent overrides &lt;code&gt;isSupported()&lt;/code&gt; to match keywords in the user's message:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DingTalkTalent&lt;/code&gt;: matches "钉钉" or "ding"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FeishuTalent&lt;/code&gt;: matches "飞书", "lark", or "feishu"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WeComTalent&lt;/code&gt;: matches "企微", "企业微信", or "wecom"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means the model automatically routes to the correct platform. If the user says &lt;em&gt;"Notify the Shanghai team on DingTalk about the deployment,"&lt;/em&gt; only &lt;code&gt;DingTalkTalent.isSupported()&lt;/code&gt; returns true, so its instruction and tools are injected. The other two Talents stay dormant.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FeishuTalent&lt;/code&gt; adds a nice touch: if a &lt;code&gt;title&lt;/code&gt; is provided, it sends an interactive card (with a blue header and lark_md body); otherwise it falls back to plain text. &lt;code&gt;DingTalkTalent&lt;/code&gt; similarly upgrades to markdown mode when a title is present.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business scenario 3: self-improving agent with long-term memory
&lt;/h2&gt;

&lt;p&gt;Perhaps the most powerful pattern is combining &lt;code&gt;MemoryTalent&lt;/code&gt; with a &lt;code&gt;ReActAgent&lt;/code&gt; to build an agent that gets smarter over time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;MemorySolutionProvider&lt;/span&gt; &lt;span class="n"&gt;solutionProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* ... configure ... */&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;MemoryTalent&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MemoryTalent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;solutionProvider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;relevanceCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// semantic matches from current conversation&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;priorityCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// high-importance fallback memories&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;relevanceInjection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// mix semantics + popularity&lt;/span&gt;

&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultTalentAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MemoryTalent&lt;/code&gt; exposes five tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memory_extract&lt;/code&gt;&lt;/strong&gt; — store a fact with an importance score (1-10). Returns the old value for comparison if the key already exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memory_recall&lt;/code&gt;&lt;/strong&gt; — exact lookup by key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memory_search&lt;/code&gt;&lt;/strong&gt; — semantic search by natural language query, or &lt;code&gt;*&lt;/code&gt; to list all keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memory_consolidate&lt;/code&gt;&lt;/strong&gt; — merge multiple low-importance fragments into a single high-importance insight (importance automatically set to 10).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memory_prune&lt;/code&gt;&lt;/strong&gt; — delete a stale or incorrect entry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;getInstruction()&lt;/code&gt; method of &lt;code&gt;MemoryTalent&lt;/code&gt; does something important: it &lt;em&gt;injects&lt;/em&gt; relevant memories into the system prompt at the start of each turn. It fetches up to &lt;code&gt;relevanceCount&lt;/code&gt; semantically similar memories and up to &lt;code&gt;priorityCount&lt;/code&gt; high-importance memories, deduplicates them, and formats them into the prompt. This means the agent doesn't need to explicitly call &lt;code&gt;memory_search&lt;/code&gt; on every turn — the most relevant context is already there.&lt;/p&gt;

&lt;p&gt;The agent is also guided by the injected instructions to proactively call &lt;code&gt;memory_extract&lt;/code&gt; when it learns something worth remembering, and &lt;code&gt;memory_consolidate&lt;/code&gt; when it notices碎片 (fragments) piling up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mount system: connecting Talents to the filesystem
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;TerminalTalent&lt;/code&gt; (the file system and bash Talent) works in concert with &lt;code&gt;MountManager&lt;/code&gt;, which maps logical paths (&lt;code&gt;@solon-source&lt;/code&gt;, &lt;code&gt;@workspace-agents&lt;/code&gt;, etc.) to physical directories. When sandbox mode is enabled, &lt;code&gt;TerminalTalent&lt;/code&gt; restricts all file operations to the work directory and explicitly listed mounts — no absolute paths, no cross-mount writes unless the mount is marked &lt;code&gt;writeable="true"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The instruction string that &lt;code&gt;TerminalTalent.getInstruction()&lt;/code&gt; generates is dynamic: it lists all active mounts with their aliases, types, and write permissions, so the LLM always knows which paths it can safely use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mount_list&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mount&lt;/span&gt; &lt;span class="na"&gt;alias=&lt;/span&gt;&lt;span class="s"&gt;"@solon-source"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"FILES"&lt;/span&gt; &lt;span class="na"&gt;writeable=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;mount&lt;/span&gt; &lt;span class="na"&gt;alias=&lt;/span&gt;&lt;span class="s"&gt;"@workspace-agents"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"AGENTS"&lt;/span&gt; &lt;span class="na"&gt;writeable=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/mount_list&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you can give an agent read-only access to your source code repositories while allowing it to write freely in its work directory — all without hardcoding path restrictions in the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why composition matters
&lt;/h2&gt;

&lt;p&gt;The Talent system's real power isn't any single Talent — it's that they compose cleanly. Each Talent is an independent module with no hard dependencies on the others. You can mix and match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;Text2SqlTalent&lt;/code&gt; to let the agent query your database in natural language.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;PdfTalent&lt;/code&gt; to let it read and summarize PDF attachments.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;CodeSearchTalent&lt;/code&gt; so it can look up API docs before writing code.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;MemoryTalent&lt;/code&gt; so it remembers your preferences across sessions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when a Talent isn't needed, you simply don't挂 it. There's no global registry to clean up, no configuration file to edit. The &lt;code&gt;isSupported()&lt;/code&gt; gate ensures even unused Talents impose zero overhead.&lt;/p&gt;

&lt;p&gt;All Talents share the same small vocabulary: &lt;code&gt;@ToolMapping&lt;/code&gt; for tools, &lt;code&gt;getInstruction()&lt;/code&gt; for context, &lt;code&gt;isSupported()&lt;/code&gt; for gating. That uniformity is what makes the system feel cohesive rather than a collection of disjoint utilities.&lt;/p&gt;

&lt;p&gt;If you want to explore the full list of built-in Talents and their APIs, the source lives at &lt;a href="https://solon.noear.org" rel="noopener noreferrer"&gt;solon.noear.org&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building a Grounded Enterprise Knowledge Base: RAG with Solon AI</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Tue, 18 Aug 2026 11:22:18 +0000</pubDate>
      <link>https://dev.to/solonjava/building-a-grounded-enterprise-knowledge-base-rag-with-solon-ai-2m1e</link>
      <guid>https://dev.to/solonjava/building-a-grounded-enterprise-knowledge-base-rag-with-solon-ai-2m1e</guid>
      <description>&lt;p&gt;Every team eventually hits the same wall with LLMs: the model is confident, fluent, and completely wrong about your internal policies, your product docs, or last quarter's numbers. It was never trained on your data, so it hallucinates plausible-sounding answers. Retrieval-Augmented Generation (RAG) is the standard fix — you retrieve the relevant facts from your own corpus first, then let the model answer &lt;em&gt;grounded&lt;/em&gt; in that context.&lt;/p&gt;

&lt;p&gt;The problem is that most RAG stacks feel like a pile of loosely-related libraries stitched together: one thing to parse PDFs, another to chunk text, a vector database SDK, an embedding client, and a prompt-assembly layer you write by hand. Solon AI (v4.0.5) collapses that whole pipeline into a small, coherent set of interfaces. This post walks through building a real enterprise knowledge base — ingesting company docs, storing them in pgvector, and answering questions grounded in the retrieved content — using only the built-in APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline in one picture
&lt;/h2&gt;

&lt;p&gt;A production RAG system has two distinct phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion (offline):&lt;/strong&gt; load raw documents → split into chunks → embed → store in a vector repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query (online):&lt;/strong&gt; embed the question → search for relevant chunks → augment the prompt → let the model answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Solon AI models each stage with a dedicated abstraction, and they compose cleanly. Let's build both phases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Ingesting documents
&lt;/h2&gt;

&lt;p&gt;Enterprise knowledge lives in messy formats — Markdown wikis, HTML pages, PDFs, Word docs, spreadsheets. Solon AI ships a &lt;code&gt;DocumentLoader&lt;/code&gt; per format as separate Maven modules (&lt;code&gt;solon-ai-load-markdown&lt;/code&gt;, &lt;code&gt;solon-ai-load-html&lt;/code&gt;, &lt;code&gt;solon-ai-load-pdf&lt;/code&gt;, &lt;code&gt;solon-ai-load-word&lt;/code&gt;, &lt;code&gt;solon-ai-load-excel&lt;/code&gt;, &lt;code&gt;solon-ai-load-ppt&lt;/code&gt;). Each loader turns raw bytes into a &lt;code&gt;List&amp;lt;Document&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Document&lt;/code&gt; is a simple, fluent value object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Document&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The refund window is 30 days from purchase."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Refund Policy"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://wiki.acme.com/policies/refund"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"department"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"support"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;metadata&lt;/code&gt; map is the workhorse for enterprise scenarios — you'll use it later to filter by department, product line, or access level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading and splitting
&lt;/h3&gt;

&lt;p&gt;Raw documents are usually too large to embed as a single vector, so they must be chunked. Solon AI provides a &lt;code&gt;SplitterPipeline&lt;/code&gt; that chains multiple &lt;code&gt;DocumentSplitter&lt;/code&gt; stages. A common, robust combination is: split on structural boundaries first (&lt;code&gt;RegexTextSplitter&lt;/code&gt;), then enforce a hard token ceiling (&lt;code&gt;TokenSizeTextSplitter&lt;/code&gt;) so no chunk exceeds the embedding model's context window.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.Document&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.DocumentLoader&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.loader.MarkdownLoader&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.loader.HtmlSimpleLoader&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.splitter.SplitterPipeline&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.splitter.RegexTextSplitter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.splitter.TokenSizeTextSplitter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;loadAndSplit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="nc"&gt;DocumentLoader&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;/html&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HtmlSimpleLoader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StandardCharsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MarkdownLoader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StandardCharsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Split structurally, then cap each chunk at 500 tokens.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SplitterPipeline&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegexTextSplitter&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TokenSizeTextSplitter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TokenSizeTextSplitter&lt;/code&gt; is not a naive character-count chopper. Internally it tokenizes with jtokkit (the &lt;code&gt;CL100K_BASE&lt;/code&gt; encoding), and when it reaches the chunk boundary it backs up to the last sentence-ending punctuation (&lt;code&gt;.&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;!&lt;/code&gt;, or newline) so chunks don't get cut mid-sentence. The default constructor targets 500 tokens per chunk, which is a sensible starting point for most embedding models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Storing in pgvector
&lt;/h2&gt;

&lt;p&gt;For the vector store, this example uses PostgreSQL with the &lt;code&gt;pgvector&lt;/code&gt; extension — a pragmatic choice for teams that already run Postgres and don't want to operate a separate vector database. Solon AI supports many backends (Redis, Elasticsearch, Milvus, Qdrant, Chroma, Weaviate, MariaDB, MySQL, and more), and they all implement the same &lt;code&gt;RepositoryStorable&lt;/code&gt; interface, so swapping backends later is a one-line change.&lt;/p&gt;

&lt;p&gt;You build a &lt;code&gt;PgVectorRepository&lt;/code&gt; from two things: an &lt;code&gt;EmbeddingModel&lt;/code&gt; and a JDBC &lt;code&gt;DataSource&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.embedding.EmbeddingModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.repository.PgVectorRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.repository.pgvector.MetadataField&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;EmbeddingModel&lt;/span&gt; &lt;span class="n"&gt;embeddingModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://127.0.0.1:11434/api/embed"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ollama"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bge-m3"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Promote hot metadata keys to real indexed columns.&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MetadataField&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;metadataFields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;metadataFields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MetadataField&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"department"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;metadataFields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MetadataField&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;metadataFields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MetadataField&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;numeric&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="nc"&gt;PgVectorRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PgVectorRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"acme_knowledge"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;metadataFields&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadataFields&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;build()&lt;/code&gt; call does the DDL heavy-lifting for you. On first initialization it runs &lt;code&gt;CREATE EXTENSION IF NOT EXISTS vector&lt;/code&gt;, creates the table with an &lt;code&gt;embedding VECTOR(n)&lt;/code&gt; column sized to your embedding model's &lt;code&gt;dimensions()&lt;/code&gt;, stores the full metadata map as &lt;code&gt;JSONB&lt;/code&gt;, and creates an &lt;code&gt;ivfflat&lt;/code&gt; cosine-distance index. The &lt;code&gt;metadataFields&lt;/code&gt; you declare get promoted to dedicated typed columns (TEXT / NUMERIC / JSONB) so you can filter on them efficiently instead of digging through JSON on every query.&lt;/p&gt;

&lt;p&gt;Saving is then trivial — the repository handles batching, embedding, and upserts internally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loadAndSplit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://wiki.acme.com/policies/refund.md"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood &lt;code&gt;save&lt;/code&gt; partitions the documents according to the embedding model's &lt;code&gt;batchSize()&lt;/code&gt;, calls the embedding model per batch, and inserts with &lt;code&gt;ON CONFLICT (id) DO UPDATE&lt;/code&gt; — so re-ingesting an updated document overwrites the old vector rather than duplicating it. For large corpora, there's an overload that reports progress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Embedded batch %d / %d%n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And an async variant, &lt;code&gt;asyncSave(docs, progressCallback)&lt;/code&gt;, that returns a &lt;code&gt;CompletableFuture&amp;lt;Void&amp;gt;&lt;/code&gt; if you want to kick ingestion off the request thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Querying with grounding
&lt;/h2&gt;

&lt;p&gt;Now the online path. The simplest retrieval is a bare query string, which uses sensible defaults (top 4 results, similarity threshold 0.4):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"How long is the refund window?"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For real applications you'll want control, and that's what &lt;code&gt;QueryCondition&lt;/code&gt; gives you. This is where enterprise metadata filtering pays off — you can restrict retrieval to a specific department while still ranking by semantic similarity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.util.QueryCondition&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;QueryCondition&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryCondition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"How long is the refund window?"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;similarityThreshold&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filterExpression&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"department == 'support' AND version &amp;gt;= 2"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;filterExpression&lt;/code&gt; is parsed by Solon's built-in expression engine (SnEL) and pushed down to the store — for pgvector it becomes a SQL &lt;code&gt;WHERE&lt;/code&gt; clause against those promoted metadata columns, so the filter runs in the database, not in your JVM after the fact. Each returned &lt;code&gt;Document&lt;/code&gt; carries a &lt;code&gt;getScore()&lt;/code&gt; reflecting its similarity to the query, so you can inspect or threshold results yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assembling the grounded prompt
&lt;/h3&gt;

&lt;p&gt;Retrieval only gets you the facts; you still have to feed them to the model. &lt;code&gt;Repository&lt;/code&gt; has a convenience method, &lt;code&gt;promptAugment&lt;/code&gt;, that does the search and packs the results into a user message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.chat.ChatModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.chat.message.ChatMessage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://127.0.0.1:11434/api/chat"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ollama"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"qwen2.5"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;ChatMessage&lt;/span&gt; &lt;span class="n"&gt;grounded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;promptAugment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"How long is the refund window?"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grounded&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;promptAugment&lt;/code&gt; builds on &lt;code&gt;ChatMessage.ofUserAugment&lt;/code&gt;, which formats the original question together with the current timestamp and the retrieved references into a single user message. The model now answers from the supplied context instead of its training-time guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further: Agentic RAG
&lt;/h2&gt;

&lt;p&gt;The pattern above is &lt;em&gt;passive&lt;/em&gt; retrieval — you decide what to search for, once, before the model runs. But some questions need the model to decide &lt;em&gt;what&lt;/em&gt; to look up, and to look up several things. Solon AI supports this with &lt;code&gt;RepositoryTool&lt;/code&gt;, which wraps a repository as a callable tool the model can invoke on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.rag.RepositoryTool&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://127.0.0.1:11434/api/chat"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ollama"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"qwen2.5"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultToolAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RepositoryTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// The model can now call `repository_query` itself, with multiple search&lt;/span&gt;
&lt;span class="c1"&gt;// terms, whenever it decides it needs background knowledge.&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Compare our refund and exchange policies, and note any version differences."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RepositoryTool&lt;/code&gt; exposes a &lt;code&gt;repository_query&lt;/code&gt; tool that accepts a list of search terms (up to five) plus a &lt;code&gt;topK&lt;/code&gt;, runs each search, and formats the merged results back to the model. This shifts the system from "passive retrieval" to "active knowledge-seeking" — the model can break a complex question into several targeted lookups. If you have a reranking model, you can pass it as a second constructor argument (&lt;code&gt;new RepositoryTool(repository, rerankingModel)&lt;/code&gt;) to reorder hits by relevance before they reach the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this composes well
&lt;/h2&gt;

&lt;p&gt;The thing worth noticing is that every stage is an interface, and swapping an implementation never touches the rest of your code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switch from pgvector to Redis or Milvus? Change the &lt;code&gt;Repository&lt;/code&gt; construction line. &lt;code&gt;save&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt;, and &lt;code&gt;promptAugment&lt;/code&gt; are identical because they're defined on &lt;code&gt;RepositoryStorable&lt;/code&gt; / &lt;code&gt;Repository&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add PDF ingestion? Add the &lt;code&gt;solon-ai-load-pdf&lt;/code&gt; dependency and swap the loader. The splitter and repository don't care where the &lt;code&gt;Document&lt;/code&gt; came from.&lt;/li&gt;
&lt;li&gt;Move from passive to agentic retrieval? Wrap the same repository in a &lt;code&gt;RepositoryTool&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That uniformity is the real payoff. RAG stops being a bespoke integration project and becomes a matter of picking implementations off a shelf, all speaking the same small vocabulary of &lt;code&gt;Document&lt;/code&gt;, &lt;code&gt;Repository&lt;/code&gt;, and &lt;code&gt;QueryCondition&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to explore the full set of loaders, splitters, and vector-store backends, the source and docs live at &lt;a href="https://solon.noear.org" rel="noopener noreferrer"&gt;solon.noear.org&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>rag</category>
    </item>
    <item>
      <title>Self-Healing Software Development: Automating Code Generation, Testing, and Fixing with Solon AI Loop Engine</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 17 Aug 2026 22:44:39 +0000</pubDate>
      <link>https://dev.to/solonjava/self-healing-software-development-automating-code-generation-testing-and-fixing-with-solon-ai-856</link>
      <guid>https://dev.to/solonjava/self-healing-software-development-automating-code-generation-testing-and-fixing-with-solon-ai-856</guid>
      <description>&lt;p&gt;Software agents are transitioning from stateless assistants to autonomous, loop-driven operators. While generating boilerplate code is easy, the real challenge in software engineering is maintaining correctness: resolving compilation errors, fixing failing unit tests, and adhering to strict quality gates. &lt;/p&gt;

&lt;p&gt;In traditional architectures, developers act as the loop execution engine: reviewing errors, editing code, and rebuilding until the tests pass. &lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;Solon AI (v4.0.5)&lt;/strong&gt;, the newly introduced &lt;code&gt;solon-ai-loop&lt;/code&gt; plugin codifies this cyclic corrective behavior directly into your backend architecture. By leveraging the &lt;strong&gt;Loop Engine&lt;/strong&gt;, custom &lt;strong&gt;Loop Strategies&lt;/strong&gt;, and automated &lt;strong&gt;Quality Gates&lt;/strong&gt;, we can construct self-healing software development agents that iterate autonomously on source code until all gates are met.&lt;/p&gt;

&lt;p&gt;In this article, we will explore the core design of the Solon AI Loop Engine and build a complete, production-grade &lt;strong&gt;Self-Healing Coding &amp;amp; Verification Pipeline&lt;/strong&gt; using Java 8+ and GraalVM-compatible components.&lt;/p&gt;




&lt;h3&gt;
  
  
  Understanding the Solon AI Loop Engine
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;solon-ai-loop&lt;/code&gt; package provides a structured framework for stateful, iterative execution of tasks. Rather than letting an LLM run indefinitely or writing custom spaghetti loops, Solon AI models loops around three main components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LoopEngine&lt;/code&gt;&lt;/strong&gt;: The core runtime that starts, pauses, resumes, and monitors loop sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LoopStrategy&lt;/code&gt;&lt;/strong&gt;: Encapsulates the state machine defining &lt;em&gt;how&lt;/em&gt; iterations progress, &lt;em&gt;when&lt;/em&gt; to check constraints, and &lt;em&gt;what&lt;/em&gt; dictates failure or success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Validator&lt;/code&gt; &amp;amp; &lt;code&gt;QualityGate&lt;/code&gt;&lt;/strong&gt;: Decoupled verification components that assert whether an output meets quality thresholds (e.g., compilation, style rules, or test suites).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three strategies are pre-built to match common software workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RalphLoopStrategy&lt;/code&gt;&lt;/strong&gt;: A PRD-driven, story-by-story implementation loop that tracks progress, learnings, and file modifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TeamPipelineStrategy&lt;/code&gt;&lt;/strong&gt;: A multi-phase transition pipeline (&lt;code&gt;PLAN&lt;/code&gt; $\rightarrow$ &lt;code&gt;PRD&lt;/code&gt; $\rightarrow$ &lt;code&gt;EXEC&lt;/code&gt; $\rightarrow$ &lt;code&gt;VERIFY&lt;/code&gt; $\rightarrow$ &lt;code&gt;FIX&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UltraQAStrategy&lt;/code&gt;&lt;/strong&gt;: A testing-focused gatekeeper that runs code checks, tracks failures, and prevents runaway token usage using &lt;strong&gt;Same-Failure Detection&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Designing a Self-Healing Development Pipeline
&lt;/h3&gt;

&lt;p&gt;Let's design a pipeline where an autonomous agent compiles, runs test suites, and refactors its own code when bugs occur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------------------------------------------------+
|                       Solon AI Loop                         |
|                                                             |
|   +----------+        +------------+        +-----------+   |
|   |  Agent   |-------&amp;gt;| Build Gate |-------&amp;gt;| Test Gate |   |
|   |  (EXEC)  |        | (VERIFY)   |        | (VERIFY)  |   |
|   +----------+        +------------+        +-----------+   |
|        ^                     |                    |         |
|        |                     v (Fail)             v (Fail)  |
|        |              +------------+        +-----------+   |
|        +--------------|  Analyze   |&amp;lt;-------| Normalize |   |
|          (Fix Loop)   |  &amp;amp; Fix     |        | Failure   |   |
|                       +------------+        +-----------+   |
+-------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To prevent the agent from getting stuck in an infinite loop trying the exact same fix on a hard-to-resolve bug, our pipeline uses the &lt;code&gt;UltraQAStrategy&lt;/code&gt;'s built-in &lt;strong&gt;Same-Failure Detection&lt;/strong&gt;. If the normalized error output remains identical across 3 consecutive attempts, the engine aborts the run with a &lt;code&gt;SAME_FAILURE&lt;/code&gt; status, saving token usage and requesting human intervention.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step-by-Step Implementation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Adding Dependencies
&lt;/h4&gt;

&lt;p&gt;Include the core Solon AI dependencies alongside the new loop engine plugin in your &lt;code&gt;pom.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Solon AI Core --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-ai-core&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.0.5&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Solon AI Loop Engine Plugin --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-ai-loop&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.0.5&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Chat Model Dialect for model integration --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;solon-ai-dialect-openai&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.0.5&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Defining the Self-Healing Code Validator
&lt;/h4&gt;

&lt;p&gt;First, we define a &lt;code&gt;Validator&lt;/code&gt; that represents our physical quality gate checks. In a real-world scenario, this validator runs local shell commands (such as &lt;code&gt;mvn compile&lt;/code&gt; or &lt;code&gt;mvn test&lt;/code&gt;) or parses project compilation structures.&lt;/p&gt;

&lt;p&gt;Here is the implementation of a mock compiler and test validator that simulates a compilation success on the 3rd attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.loop.validator.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SelfHealingCodeValidator&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Validator&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;compileAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ValidationCriteria&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;validateIteration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt; &lt;span class="nf"&gt;validateQualityGate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QualityGate&lt;/span&gt; &lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Evaluate based on the gate type ("build" or "test")&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"build"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;compileAttempts&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compileAttempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Compilation Error"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
                    &lt;span class="s"&gt;"error: cannot find symbol\n  symbol: class OrderProcessor\n  location: package com.demo"&lt;/span&gt;
                &lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;passed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Build success!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;passed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"All unit tests passed!"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;passed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quality check skipped."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt; &lt;span class="nf"&gt;validateIteration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;iterationResult&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ValidationContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterationResult&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Execution Result Null"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"No artifacts were produced."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ValidationResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;passed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Iteration valid."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Configuring the Loop Session with UltraQA Strategy
&lt;/h4&gt;

&lt;p&gt;Now, we set up the &lt;code&gt;LoopEngine&lt;/code&gt; and use &lt;code&gt;UltraQAStrategy&lt;/code&gt; to drive our self-healing loop. We'll target the &lt;code&gt;TESTS&lt;/code&gt; goal type and configure a maximum of 5 overall iterations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.loop.config.LoopConfig&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.loop.config.LoopEngineConfig&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.loop.engine.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.loop.strategy.UltraQAStrategy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.loop.strategy.UltraQAStrategy.UltraQAGoalType&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.loop.strategy.UltraQAStrategy.UltraQAExitReason&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.loop.validator.QualityGate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.time.Duration&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Arrays&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.HashMap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SelfHealingPipelineDemo&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;InterruptedException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Initialize the Loop Engine&lt;/span&gt;
        &lt;span class="nc"&gt;LoopEngineConfig&lt;/span&gt; &lt;span class="n"&gt;engineConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoopEngineConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;monitoringEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debuggingEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;LoopEngine&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimpleLoopEngine&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engineConfig&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Configure the Quality Gates (Build Gate followed by Test Gate)&lt;/span&gt;
        &lt;span class="nc"&gt;QualityGate&lt;/span&gt; &lt;span class="n"&gt;buildGate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QualityGate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Runs compilation &amp;amp; dependency validation&lt;/span&gt;
        &lt;span class="nc"&gt;QualityGate&lt;/span&gt; &lt;span class="n"&gt;testGate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QualityGate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// Runs unit &amp;amp; integration tests&lt;/span&gt;

        &lt;span class="c1"&gt;// 3. Configure the QA Loop Strategy&lt;/span&gt;
        &lt;span class="nc"&gt;UltraQAStrategy&lt;/span&gt; &lt;span class="n"&gt;qaStrategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UltraQAStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gates&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Arrays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buildGate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testGate&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;goalType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UltraQAGoalType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TESTS&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parallelTesting&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxTestAttempts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Max cycles&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;strictMode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// 4. Bind configuration&lt;/span&gt;
        &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"workDir"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"./demo-project"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;LoopConfig&lt;/span&gt; &lt;span class="n"&gt;loopConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoopConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;taskDescription&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fix missing OrderProcessor class implementation in com.demo"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qaStrategy&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SelfHealingCodeValidator&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxIterations&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verificationRequired&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;statePersistenceEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// 5. Start the Loop Session&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Starting Self-Healing pipeline..."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;LoopSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loopConfig&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 6. Listen to live iteration state updates&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onIterationComplete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterResult&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[Iteration %d] Status: %s | Message: %s | Duration: %d ms\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;iterResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNumber&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;iterResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getState&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;iterResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;iterResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDuration&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toMillis&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Check metadata parameters&lt;/span&gt;
            &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iterResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containsKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failures"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  -&amp;gt; Cumulative Failures Recorded: %s\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failures"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onStateChange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[State Event] Loop transitioned to: %s\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// 7. Await termination (max 10 seconds)&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitForCompletion&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// 8. Print out final analysis&lt;/span&gt;
        &lt;span class="nc"&gt;LoopResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getResult&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n===================================="&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Pipeline Final Summary:"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Session ID: %s\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSessionId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Successful: %s\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSuccess&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Final State: %s\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFinalState&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Total Iterations Run: %d\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalIterations&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Total Execution Time: %d ms\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalDuration&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toMillis&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

            &lt;span class="nc"&gt;UltraQAExitReason&lt;/span&gt; &lt;span class="n"&gt;exitReason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qaStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFinalExitReason&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Execution Termination Reason: %s (%s)\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
                    &lt;span class="n"&gt;exitReason&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; 
                    &lt;span class="n"&gt;exitReason&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDescription&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"===================================="&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Exploring the Run Traces
&lt;/h3&gt;

&lt;p&gt;When running the pipeline, the console outputs how the state machine dynamically handles failures, fixes, and gates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting Self-Healing pipeline...
[State Event] Loop transitioned to: EXECUTING
[Iteration 1] Status: FIXING | Message: Gate failed: build | Duration: 4 ms
  -&amp;gt; Cumulative Failures Recorded: 1
[State Event] Loop transitioned to: FIXING
[Iteration 2] Status: FIXING | Message: Gate failed: build | Duration: 2 ms
  -&amp;gt; Cumulative Failures Recorded: 2
[State Event] Loop transitioned to: FIXING
[Iteration 3] Status: VERIFYING | Message: Gate failed: test | Duration: 1 ms
  -&amp;gt; Cumulative Failures Recorded: 2
[Iteration 4] Status: COMPLETED | Message: All quality gates passed | Duration: 2 ms
  -&amp;gt; Cumulative Failures Recorded: 2
[State Event] Loop transitioned to: COMPLETED

====================================
Pipeline Final Summary:
  Session ID: 4c3ab871-6c2e-4b20-80de-cd13ad91cf76
  Successful: true
  Final State: COMPLETED
  Total Iterations Run: 4
  Total Execution Time: 9 ms
  Execution Termination Reason: GOAL_MET (目标达成)
====================================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  How it resolved:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 1 &amp;amp; 2&lt;/strong&gt;: The compile validator returned &lt;code&gt;Compilation Error&lt;/code&gt;. Because the gate failed, the loop state changed to &lt;code&gt;FIXING&lt;/code&gt;, indicating to the agent that corrective steps were needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 3&lt;/strong&gt;: On the 3rd attempt, the compile check passed. The runner moved on to check the &lt;code&gt;test&lt;/code&gt; gate, which failed because the test suite had not yet run under the verified state. The loop transitioned to &lt;code&gt;VERIFYING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration 4&lt;/strong&gt;: Both gates were fully verified, culminating in &lt;code&gt;All quality gates passed&lt;/code&gt; and terminating with &lt;code&gt;GOAL_MET&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Under the Hood: Same-Failure Guard &amp;amp; Message Normalization
&lt;/h3&gt;

&lt;p&gt;If we simulate a recurring bug where the agent repeats the exact same error, the pipeline guards against runaway execution.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;UltraQAStrategy&lt;/code&gt; class implements normalization rules to sanitize error logs before registering them. Dynamic parameters like timestamps, random file lines, execution times, and version numbers are removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;normalizeFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failure&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replaceAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Remove ISO timestamps&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replaceAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":\\d+:\\d+"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                        &lt;span class="c1"&gt;// Remove line:column numbers&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replaceAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\d+ms"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                              &lt;span class="c1"&gt;// Remove durations&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replaceAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"line \\d+"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"line N"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;// Generalize line numbers&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replaceAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\s+"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                            &lt;span class="c1"&gt;// Collapse white spaces&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;failures.get(i).equals(lastFailure)&lt;/code&gt; matches the same pattern 3 times consecutively, the engine breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Iteration 1] Status: FIXING | Message: Gate failed: build | Error: compilation error at line 42
[Iteration 2] Status: FIXING | Message: Gate failed: build | Error: compilation error at line 42
[Iteration 3] Status: FIXING | Message: Gate failed: build | Error: compilation error at line 42

Execution Termination Reason: SAME_FAILURE (相同的失败重复出现)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By adding this check, you keep your LLM agent from entering a repeating loop when encountering system errors or environmental problems, preventing unnecessary API costs.&lt;/p&gt;




&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The Loop Engine in Solon AI (v4.0.5) shifts agent design from stateless, unidirectional workflows to robust, self-verifying systems. By pairing structural LLM generation with local compiling/testing tools via &lt;code&gt;UltraQAStrategy&lt;/code&gt; or &lt;code&gt;TeamPipelineStrategy&lt;/code&gt;, you can construct enterprise-grade pipelines capable of autonomous bug resolution.&lt;/p&gt;

&lt;p&gt;Integrate the &lt;code&gt;solon-ai-loop&lt;/code&gt; plugin into your current codebase and begin building self-healing, agentic workflows today!&lt;/p&gt;

&lt;p&gt;For more documentation and code examples, visit the official repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/opensolon/solon-ai" rel="noopener noreferrer"&gt;opensolon/solon-ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official Website&lt;/strong&gt;: &lt;a href="https://solon.noear.org" rel="noopener noreferrer"&gt;solon.noear.org&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>Securing Financial Agents: Implementing Human-in-the-Loop (HITL) Workflows in Solon AI</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 17 Aug 2026 17:07:53 +0000</pubDate>
      <link>https://dev.to/solonjava/securing-financial-agents-implementing-human-in-the-loop-hitl-workflows-in-solon-ai-2jam</link>
      <guid>https://dev.to/solonjava/securing-financial-agents-implementing-human-in-the-loop-hitl-workflows-in-solon-ai-2jam</guid>
      <description>&lt;p&gt;In the era of autonomous AI Agents, letting a Large Language Model (LLM) invoke backend tools directly presents a significant risk. If an Agent receives a command to "transfer $10,000 to user X" or "permanently purge the user database," executing it without human verification could result in financial or data disaster. &lt;/p&gt;

&lt;p&gt;To bridge the gap between AI autonomy and enterprise safety, &lt;strong&gt;Solon AI&lt;/strong&gt; (v4.0.5) introduces a robust, native &lt;strong&gt;Human-in-the-Loop (HITL)&lt;/strong&gt; framework. By utilizing session-aware interceptors, it intercepts high-risk tool calls, suspends execution, and exposes an API for administrators to approve, reject, skip, or modify arguments before resuming.&lt;/p&gt;

&lt;p&gt;This guide explores the design, API structure, and practical implementation of HITL workflows in Solon AI.&lt;/p&gt;




&lt;h3&gt;
  
  
  The HITL Architecture in Solon AI
&lt;/h3&gt;

&lt;p&gt;The HITL architecture is designed around the &lt;strong&gt;ReAct (Reasoning and Acting)&lt;/strong&gt; loop. Instead of letting the Agent execute tools immediately after reasoning, an interceptor chain checks the arguments against predefined safety rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[User Request] 
      │
      ▼
 [ReActAgent] (Reasoning)
      │
      ▼
  [ToolCall] ────► [HITLInterceptor] (Evaluate Args)
                          │
                ┌─────────┴─────────┐
         [Allowed / Safe]    [High-Risk / Suspend]
                │                   │
                ▼                   ├─► Push HITLPendingEvent
          [Execute Tool]            ├─► Set Session to PENDING
                │                   ▼
                │           [Wait for Human Decision]
                │                   │
                │                   ▼  (via API Controller)
                │           [HITL.submit(...) / approve / reject]
                │                   │
                │                   ├─► Push HITLDecidedEvent
                │                   ├─► Apply modifiedArgs / skip / reject
                │                   ▼
                └───────────────► [Resume Execution]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At its core, Solon AI provides three main components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HITLInterceptor&lt;/code&gt;&lt;/strong&gt;: ReAct interceptor that registers target tools and runs strategies to decide if a request should be suspended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HITL&lt;/code&gt;&lt;/strong&gt;: A static helper class offering convenient APIs for your controller to inspect pending tasks and submit human decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HITLDecision&lt;/code&gt;&lt;/strong&gt;: An object containing the decision type (Approve, Reject, Skip), comments, and optional parameter modifications.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Step 1: Defining a High-Risk Tool
&lt;/h3&gt;

&lt;p&gt;Let's assume we have a Java tool mapped to execute database transfers. In Solon AI, this is defined as a standard tool provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.chat.tool.AbsToolProvider&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.chat.tool.annotation.ToolMapping&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FinancialTools&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbsToolProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@ToolMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"transfer"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Transfer money to another account"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;targetAccount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Execute the transfer transaction&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Successfully transferred $%.2f to %s\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetAccount&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"SUCCESS"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 2: Configuring the HITL Interceptor
&lt;/h3&gt;

&lt;p&gt;An interceptor is attached to the &lt;code&gt;ReActAgent&lt;/code&gt; during construction. We register our safety evaluation rules using the &lt;code&gt;HITLStrategy&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.react.ReActAgent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.react.intercept.HITLInterceptor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.react.intercept.HITLStrategy&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Setup ReActAgent with HITL Interceptor&lt;/span&gt;
&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llmModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultToolAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FinancialTools&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultInterceptorAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HITLInterceptor&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;// Define a strategy for the "transfer" tool&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onTool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"transfer"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parseDouble&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="c1"&gt;// If the amount exceeds $5,000, trigger HITL interception&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"The transfer amount exceeds the single-transaction limit of $5000."&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Safe to proceed without intervention&lt;/span&gt;
        &lt;span class="o"&gt;})&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the LLM outputs a tool call to &lt;code&gt;transfer&lt;/code&gt; with an amount greater than &lt;code&gt;5000&lt;/code&gt;, the &lt;code&gt;HITLInterceptor&lt;/code&gt; intercepts it, blocks execution, and tags the &lt;code&gt;AgentSession&lt;/code&gt; as &lt;strong&gt;Pending&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Handling Suspension in the Controller
&lt;/h3&gt;

&lt;p&gt;When exposing the Agent through an HTTP endpoint, your controller must handle the &lt;code&gt;PENDING&lt;/code&gt; state. Under the hood, Solon AI stores the task snapshots in the session context, which can be fetched using the &lt;code&gt;HITL&lt;/code&gt; helper class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.annotation.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.AgentSession&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.react.ReActResponse&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.react.intercept.HITL&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.react.intercept.HITLTask&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.core.handle.Result&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Controller&lt;/span&gt;
&lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/agent"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Injected or constructed&lt;/span&gt;

    &lt;span class="nd"&gt;@Post&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ask"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;AgentSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getOrCreateSession&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Run the agent loop&lt;/span&gt;
        &lt;span class="nc"&gt;ReActResponse&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// If the execution was intercepted by HITL, return the pending task details&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isPending&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;HITLTask&lt;/span&gt; &lt;span class="n"&gt;pendingTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HITL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPendingTask&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"REQUIRED_HUMAN_APPROVAL"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pendingTask&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;succeed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent decides to transfer &lt;code&gt;$12,000&lt;/code&gt;, the REST API returns a status showing that approval is required, along with the &lt;code&gt;HITLTask&lt;/code&gt; metadata (including the unique &lt;code&gt;callUuid&lt;/code&gt;, the tool name, and the arguments).&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4: Submitting Human Decisions
&lt;/h3&gt;

&lt;p&gt;Administrators need an endpoint to approve, reject, or bypass the transaction. With Solon AI v4.0.5, decisions are bound to a specific &lt;code&gt;callUuid&lt;/code&gt; to handle parallel or batch tool calls.&lt;/p&gt;

&lt;p&gt;Here is how you handle approval, rejection, skipping, and &lt;strong&gt;parameter modification&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.react.intercept.HITLDecision&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Post&lt;/span&gt;
&lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/approve"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt; &lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;callUuid&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;@Body&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;modifiedArgs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;AgentSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getSession&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Locate the precise task using callUuid&lt;/span&gt;
    &lt;span class="nc"&gt;HITLTask&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HITL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPendingTaskByCallUuid&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callUuid&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No pending task found for UUID: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;callUuid&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;HITLDecision&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"approve"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equalsIgnoreCase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HITLDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;approve&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Approved by operations manager."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Human Override: If the manager adjusted the transfer amount down, apply it!&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifiedArgs&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;modifiedArgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modifiedArgs&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifiedArgs&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"reject"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equalsIgnoreCase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HITLDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Rejected: Suspected fraudulent activity."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Skip: Bypass this tool call, returning a custom mock message to the LLM&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HITLDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;skip&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Skipped: Submitter bypassed this step."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Submit the decision back to the session&lt;/span&gt;
    &lt;span class="no"&gt;HITL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Resume execution: Trigger agent call with a blank prompt&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ReActResponse&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;succeed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Inside the Decision Lifecycle
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;HITL.submit(...)&lt;/code&gt; is called, the decision is stored in the &lt;code&gt;AgentSession&lt;/code&gt; context under &lt;code&gt;_hitl_decision_&amp;lt;callUuid&amp;gt;&lt;/code&gt;. When the Agent resumes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onAgentStart(trace)&lt;/code&gt;&lt;/strong&gt;: The interceptor intercepts the resume event. It scans all pending tasks in the session. If all have decisions, it sets the routing directly to the &lt;code&gt;ACTION&lt;/code&gt; phase, skipping another LLM reasoning step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ActionTask&lt;/code&gt;&lt;/strong&gt;: When executing the tool task:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Approve&lt;/strong&gt;: The tool is invoked. If &lt;code&gt;modifiedArgs&lt;/code&gt; are provided, the interceptor clones the original arguments to preserve history, applies the modified parameters, and invokes the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Reject&lt;/strong&gt;: The tool is blocked. A rejection message is injected into the exchange context as if the tool failed, steering the LLM to handle the denial gracefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Skip&lt;/strong&gt;: The tool is not run. The &lt;code&gt;comment&lt;/code&gt; from the decision is directly returned to the LLM as the mock result of the tool.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Batch Tool Call &amp;amp; AlwaysAllow Settings
&lt;/h3&gt;

&lt;p&gt;Solon AI's HITL framework also supports advanced features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch Approvals&lt;/strong&gt;: If the model decides to invoke three tools simultaneously, &lt;code&gt;HITL.getPendingTasks(session)&lt;/code&gt; returns a list of tasks. You can submit individual decisions using &lt;code&gt;HITL.submitAll(session, Map&amp;lt;callUuid, HITLDecision&amp;gt;)&lt;/code&gt; or approve them all with &lt;code&gt;HITL.approveAll(session)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;alwaysAllow&lt;/code&gt;&lt;/strong&gt;: In some cases, a manager wants to say "Approve this transfer, and trust this specific action for the rest of the session." By submitting &lt;code&gt;HITLDecision.approve(true)&lt;/code&gt; (or &lt;code&gt;.alwaysAllow(true)&lt;/code&gt;), the framework automatically registers a session-level rule to bypass subsequent checks for this specific tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;The Human-in-the-Loop mechanism in Solon AI (v4.0.5) transforms risky AI operations into secure, auditable, and human-guided workflows. By placing interceptors right at the boundary of tool execution and enabling real-time argument overrides, Solon AI ensures that autonomous agents remain safely within corporate compliance guardrails.&lt;/p&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Orchestrating Complex Enterprise Workflows with Nested Agents in Solon AI</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:59:41 +0000</pubDate>
      <link>https://dev.to/solonjava/orchestrating-complex-enterprise-workflows-with-nested-agents-in-solon-ai-4030</link>
      <guid>https://dev.to/solonjava/orchestrating-complex-enterprise-workflows-with-nested-agents-in-solon-ai-4030</guid>
      <description>&lt;p&gt;Standard monolithic Large Language Model (LLM) prompts fall short when faced with complex, multi-stage enterprise procedures. For workflows such as financial credit approvals or audit pipelines, tasks must be routed based on strict compliance rules, pass through hierarchical review loops, and sometimes return to previous steps for corrections. &lt;/p&gt;

&lt;p&gt;In this article, we demonstrate how to build and orchestrate a multi-stage &lt;strong&gt;Financial Loan Approval Pipeline&lt;/strong&gt; using &lt;strong&gt;Solon AI&lt;/strong&gt; (v4.0.5). We will leverage its native integration with the lightweight state machine engine &lt;strong&gt;Solon Flow&lt;/strong&gt;, utilizing nested agents, conditional routing (Exclusive Junctions), and state verification.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Architecture: Nested Agents and Flow Orchestration
&lt;/h3&gt;

&lt;p&gt;Our credit approval system simulates a real-world enterprise workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Entry Officer (KYC)&lt;/strong&gt;: Performs preliminary verification and data formatting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loan Architect&lt;/strong&gt;: Automatically designs loan products, interest rates, and loan terms, rating risk levels (&lt;code&gt;high&lt;/code&gt;, &lt;code&gt;mid&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk Control Center (Nested Team)&lt;/strong&gt;: A specialized, nested sub-agent committee that acts as a unified black box. Inside, a &lt;strong&gt;Credit Department&lt;/strong&gt; sub-team, a &lt;strong&gt;Quota Calculator&lt;/strong&gt;, and a &lt;strong&gt;Legal Compliance Officer&lt;/strong&gt; collaborate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stress Tester&lt;/strong&gt;: Run simulations to assess risk and default probabilities. If tests fail, the workflow loops back to the Risk Control Center for term rectification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Auditor&lt;/strong&gt;: Conducts final checks to ensure GDPR compliance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final Approver&lt;/strong&gt;: Signs off on the final decision, determining the disbursement route (&lt;code&gt;canary&lt;/code&gt; or &lt;code&gt;full&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a simplified flowchart of the process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Start] -&amp;gt; (Entry Officer) -&amp;gt; (Loan Architect) -&amp;gt; [Junction: Risk Level?]
                                                        |
                            +---------------------------+ (high risk)
                            |                           |
                            v                           v (low/mid risk)
                   ((Risk Control Center)) ----&amp;gt; (Stress Tester)
                            ^                           |
                            |                           v
                            +----------------- [Junction: Passed?] (no)
                                                        | (yes)
                                                        v
                                                (Security Auditor)
                                                        |
                                                        v
                                                (Final Approver) -&amp;gt; [Disbursement Route] -&amp;gt; [End]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Implementing the Workflow in Solon AI
&lt;/h3&gt;

&lt;p&gt;Let us implement this pipeline in Java using Solon AI.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Initialize Chat Model and Define ReAct Agents
&lt;/h4&gt;

&lt;p&gt;First, we set up our LLM backend and instantiate the individual professional agents using &lt;code&gt;ReActAgent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.chat.ChatModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.react.ReActAgent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Initialize the ChatModel&lt;/span&gt;
&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.your-provider.com/v1/chat/completions"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"your-api-key"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"your-model-name"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Entry Officer (KYC Analyst)&lt;/span&gt;
&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;entryOfficer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"entry_officer"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Loan Intake Officer"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Verify customer identity. Structure the application details cleanly. Ask for missing details."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Loan Architect&lt;/span&gt;
&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;loanArchitect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loan_architect"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Credit Product Architect"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Calculate DTI (Debt-to-Income). Assign preliminary risk label: [high, mid, low]."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Build a Nested Risk Control Committee
&lt;/h4&gt;

&lt;p&gt;In Solon AI, a &lt;code&gt;TeamAgent&lt;/code&gt; acts as a multi-agent container. Since a team is itself an instance of &lt;code&gt;Agent&lt;/code&gt;, it can be nested inside another &lt;code&gt;TeamAgent&lt;/code&gt; seamlessly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.team.TeamAgent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Risk Control Center containing multiple internal experts and a nested Credit Dept sub-team&lt;/span&gt;
&lt;span class="nc"&gt;TeamAgent&lt;/span&gt; &lt;span class="n"&gt;riskControlCenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TeamAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"risk_control_center"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Deep Risk and Compliance Assessment Group"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="c1"&gt;// Nested sub-team (Credit Department)&lt;/span&gt;
                &lt;span class="nc"&gt;TeamAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"credit_dept"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                                &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"big_data_analyst"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Big Data Risk Modeler"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Analyze social behavior patterns and alternative credit scores."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;

                &lt;span class="c1"&gt;// Quota Calculator&lt;/span&gt;
                &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"quota_calculator"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Credit Actuary"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Evaluate collateral and financial records to determine the final credit line."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;

                &lt;span class="c1"&gt;// Legal Compliance Officer&lt;/span&gt;
                &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"legal_reviewer"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AML Auditor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cross-check PEP lists and perform Anti-Money Laundering verification."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Define Post-Assessment and Audit Agents
&lt;/h4&gt;

&lt;p&gt;Next, define the validation, stress-testing, and sign-off actors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;riskStressTester&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"risk_stress_tester"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Stress Test Engineer"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Simulate default probability under a +200BP interest rate shock. Set 'passed' to true/false."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;securityAuditor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"security_auditor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Data Privacy Auditor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Verify that user data handling adheres to GDPR and national data security laws."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;ReActAgent&lt;/span&gt; &lt;span class="n"&gt;finalApprover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReActAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"final_approver"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"General Manager of Credit Division"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Synthesize the stress test and security audit to output a final strategy: [canary, full, reject]."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 4: Programmatic Flow Orchestration (&lt;code&gt;graphAdjuster&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Now, orchestrate the interaction flow. By default, &lt;code&gt;TeamAgent&lt;/code&gt; uses collaborative protocols (e.g., &lt;code&gt;Sequential&lt;/code&gt;, &lt;code&gt;Swarm&lt;/code&gt;), but for strict corporate governance, we override the topology using &lt;code&gt;.graphAdjuster&lt;/code&gt; to build a directed workflow graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TeamAgent&lt;/span&gt; &lt;span class="n"&gt;creditApprovalSystem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TeamAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"credit_approval_system"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;graphAdjuster&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Start node routes directly to the KYC officer&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addStart&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"entry_officer"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActivity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entryOfficer&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loan_architect"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Define conditional branching: high-risk applications route to the Wind Control Center&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addExclusive&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"exc_risk_level"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"risk_control_center"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"risk == 'high'"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"risk_stress_tester"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Wind Control Center transitions to the Stress Tester&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActivity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;riskControlCenter&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"risk_stress_tester"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Stress Tester branches depending on outcome. If failed (passed == false), return to Risk Center&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActivity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;riskStressTester&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"exc_test_result"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addExclusive&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"exc_test_result"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"risk_control_center"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"passed == false"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"security_auditor"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActivity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;securityAuditor&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"final_approver"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActivity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finalApprover&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"exc_release"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Branch to final disbursement channels&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addExclusive&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"exc_release"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"canary_disburser"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"route == 'canary'"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"full_disburser"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"route == 'full'"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActivity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"canary_disburser"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Canary Disbursement Channel"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addActivity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"full_disburser"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Full-Scale Disbursement Channel"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;linkAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addEnd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Running the Workflow with State Tracking
&lt;/h3&gt;

&lt;p&gt;To execute our orchestrated agent network, we maintain a persistent state using &lt;code&gt;AgentSession&lt;/code&gt;. This keeps a record of variables like &lt;code&gt;risk&lt;/code&gt;, &lt;code&gt;passed&lt;/code&gt;, and &lt;code&gt;route&lt;/code&gt;, ensuring they flow correctly between nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.AgentSession&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.session.InMemoryAgentSession&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.chat.prompt.Prompt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.ai.agent.team.TeamTrace&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Create an in-memory session for a loan application&lt;/span&gt;
        &lt;span class="nc"&gt;AgentSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryAgentSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"LOAN_ID_2026_999"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"【Urgent Loan Application】\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s"&gt;"Applicant: Wang Wu (Business Owner)\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s"&gt;"Purpose: 8 Million RMB for raw materials purchasing.\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s"&gt;"Note: Since this involves cross-border trade and a high limit, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                &lt;span class="s"&gt;"please tag this as high risk. Verify disbursement via the Canary channel."&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Run the workflow&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;creditApprovalSystem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Prompt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"=== Final Execution Report ===\n"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Inspect and trace the execution path&lt;/span&gt;
        &lt;span class="nc"&gt;TeamTrace&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;creditApprovalSystem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTrace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n=== Execution Path Trace ==="&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRecords&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; 
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Executed Node: ["&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSource&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"]"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Sample Traced Path Output:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== Execution Path Trace ===
Executed Node: [entry_officer]
Executed Node: [loan_architect]
Executed Node: [risk_control_center]
Executed Node: [risk_stress_tester]
Executed Node: [security_auditor]
Executed Node: [final_approver]
Executed Node: [canary_disburser]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Due to the applicant's status and the high transaction limit, the system dynamically rerouted the execution thread through the &lt;code&gt;risk_control_center&lt;/code&gt; before performing stress tests and releasing funds through the canary gateway.&lt;/p&gt;




&lt;h3&gt;
  
  
  Architectural Advantages
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic Controls for Nondeterministic LLMs&lt;/strong&gt;: By defining structured states (using &lt;code&gt;addExclusive&lt;/code&gt; and SpEL conditions), the workflow enforces rigid banking boundaries while allowing LLMs to handle unstructured text processing within individual nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialized Division of Labor&lt;/strong&gt;: High-risk calculations are isolated inside the &lt;code&gt;risk_control_center&lt;/code&gt; micro-agent block. General tasks skip this step, optimizing latency and saving LLM token costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Loop Corrections&lt;/strong&gt;: If the stress test flags default vulnerability (&lt;code&gt;passed == false&lt;/code&gt;), the agent automatically returns to the risk center to renegotiate security collaterals without crashing the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete Trace Audits&lt;/strong&gt;: Since every single execution step is tracked inside &lt;code&gt;TeamTrace&lt;/code&gt;, banking compliance officers can easily audit the trace record log, mapping out exactly which agent made which decision.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>ai</category>
      <category>workflow</category>
    </item>
    <item>
      <title>How Solon Bypasses Heavy Classpath Scanning in GraalVM Native Image using IndexFiles</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:44:10 +0000</pubDate>
      <link>https://dev.to/solonjava/how-solon-bypasses-heavy-classpath-scanning-in-graalvm-native-image-using-indexfiles-58gf</link>
      <guid>https://dev.to/solonjava/how-solon-bypasses-heavy-classpath-scanning-in-graalvm-native-image-using-indexfiles-58gf</guid>
      <description>&lt;p&gt;Classpath scanning is a ubiquitous practice in modern Java frameworks. When your application starts, the framework scans JARs, reads directories, and looks for classes annotated with &lt;code&gt;@Component&lt;/code&gt;, &lt;code&gt;@Controller&lt;/code&gt;, or XML mapper files. &lt;/p&gt;

&lt;p&gt;While this dynamic approach works wonderfully on standard JVMs, it poses a severe bottleneck for &lt;strong&gt;GraalVM Native Images&lt;/strong&gt;. The closed-world assumption of GraalVM mandates that all classes, reflections, and resources must be known at compile time. Furthermore, traditional classpath scanning (&lt;code&gt;ClassLoader.getResources(...)&lt;/code&gt;) is either highly restricted or incredibly slow in a compiled native binary.&lt;/p&gt;

&lt;p&gt;In this article, we'll dive deep into &lt;strong&gt;Solon's AOT (Ahead-of-Time) compilation engine&lt;/strong&gt; and see how it solves classpath scanning under GraalVM Native Image using a lightweight, elegant mechanism called &lt;strong&gt;IndexFiles&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  The GraalVM Native Image Dilemma
&lt;/h3&gt;

&lt;p&gt;To compile a Java application into a standalone executable, GraalVM needs to build a static dependency graph starting from the &lt;code&gt;main&lt;/code&gt; entry point. Any class accessed via reflection, any dynamic proxy, and any resource file must be registered beforehand in JSON configuration files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reflect-config.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resource-config.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serialization-config.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;proxy-config.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your framework relies on scanning directory trees inside JAR files at runtime to discover components, it will fail under Native Image—there are no JAR files at runtime!&lt;/p&gt;

&lt;p&gt;While some frameworks solve this by generating massive amounts of source code or bytecode during compilation, Solon takes a cleaner, more runtime-friendly approach: &lt;strong&gt;IndexFiles&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Introducing Solon's IndexFiles
&lt;/h3&gt;

&lt;p&gt;Solon introduces &lt;code&gt;org.noear.solon.core.runtime.IndexFiles&lt;/code&gt;, a internal helper class designed to record scans during build time and substitute them with static indexes at runtime.&lt;/p&gt;

&lt;p&gt;The core idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;At AOT Compilation Time&lt;/strong&gt;: Run the Solon container in a special AOT-processing mode to intercept and record all classpath scans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write Flat Index Files&lt;/strong&gt;: Save these scan results into &lt;code&gt;META-INF/solon-index/&lt;/code&gt; as simple &lt;code&gt;.index&lt;/code&gt; text files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Runtime&lt;/strong&gt;: Bypass expensive scanning. Read the &lt;code&gt;.index&lt;/code&gt; files directly and load the recorded classes or resources instantly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This mechanism applies to both &lt;strong&gt;class scanning&lt;/strong&gt; (discovering beans) and &lt;strong&gt;resource scanning&lt;/strong&gt; (finding templates, config files, or RPC descriptors).&lt;/p&gt;




&lt;h3&gt;
  
  
  How It Works: Step-by-Step
&lt;/h3&gt;

&lt;p&gt;Let's look at the underlying implementation details.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The AOT Flag Interceptor
&lt;/h4&gt;

&lt;p&gt;During Solon's AOT phase, the maven/gradle plugin triggers the main execution via &lt;code&gt;org.noear.solon.aot.SolonAotProcessor&lt;/code&gt;. The processor sets a crucial system property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NativeDetector&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AOT_PROCESSING&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the container that it is running in build-time AOT pre-processing mode.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Intercepting Class Scanning
&lt;/h4&gt;

&lt;p&gt;When Solon scans for beans, it uses &lt;code&gt;ClassUtil.scanClasses(clzExpr)&lt;/code&gt;. Let's look at how it behaves under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;scanClasses&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClassLoader&lt;/span&gt; &lt;span class="n"&gt;classLoader&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clzExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Consumer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clzConsumer&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NativeDetector&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAotRuntime&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Build-time AOT Processing&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clzNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;doScanClasses0&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classLoader&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clzExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clz&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;clzNames&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;clzConsumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clz&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
        &lt;span class="c1"&gt;// Write the recorded scan results to an index file&lt;/span&gt;
        &lt;span class="nc"&gt;IndexFiles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeIndexFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clzExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"scan_clz"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clzNames&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Standard JVM or Native Image Runtime&lt;/span&gt;
        &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clzNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IndexFiles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadIndexFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clzExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"scan_clz"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clzNames&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Index exists! Directly load classes from the pre-recorded index&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clzName&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clzNames&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ClassUtil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadClass&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classLoader&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clzName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clz&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;clzConsumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clz&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// Fallback to slow scan if no index exists&lt;/span&gt;
        &lt;span class="n"&gt;doScanClasses0&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classLoader&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clzExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clz&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clzConsumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clz&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By substituting raw scanning with a pre-built index, Solon bypasses JAR scanning entirely!&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Intercepting Resource Scanning
&lt;/h4&gt;

&lt;p&gt;The exact same logic applies to resource scanning via &lt;code&gt;ResourceUtil.scanResources&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;scanResources&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClassLoader&lt;/span&gt; &lt;span class="n"&gt;classLoader&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;resExpr&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NativeDetector&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAotRuntime&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;resList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;scanResources&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classLoader&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;resList:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Write to an index file, e.g., mapping to a "scan_res" tag&lt;/span&gt;
        &lt;span class="nc"&gt;IndexFiles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeIndexFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"scan_res"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resList&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resList&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;resList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IndexFiles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadIndexFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"scan_res"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resList&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;resList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
            &lt;span class="n"&gt;scanResources&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classLoader&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resExpr&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;resList:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resList&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Anatomy of an Index File
&lt;/h3&gt;

&lt;p&gt;How does Solon serialize these expressions into filenames? Filesystem paths have strict character limitations, whereas package patterns or scan expressions contain wildcard asterisks (&lt;code&gt;*&lt;/code&gt;) and colons (&lt;code&gt;:&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IndexFiles.getIndexFileName&lt;/code&gt; safely sanitizes expressions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dots (&lt;code&gt;.&lt;/code&gt;), slashes (&lt;code&gt;/&lt;/code&gt;), and backslashes (&lt;code&gt;\&lt;/code&gt;) are replaced with hyphens (&lt;code&gt;-&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Asterisks (&lt;code&gt;*&lt;/code&gt;) are replaced with at-signs (&lt;code&gt;@&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Colons (&lt;code&gt;:&lt;/code&gt;) are replaced with exclamation marks (&lt;code&gt;!&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The filename is post-fixed with &lt;code&gt;_{tag}.index&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a scan expression like &lt;code&gt;classpath:demo/**/*.json&lt;/code&gt; (mapped to tag &lt;code&gt;scan_res&lt;/code&gt;) will generate an index file located at:&lt;br&gt;
&lt;code&gt;META-INF/solon-index/classpath!demo--@@-@.json_scan_res.index&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Inside the file is a simple list of matching resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;demo/config/db.json
demo/static/data.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When running in GraalVM Native Image, &lt;code&gt;ResourceUtil&lt;/code&gt; simply opens this text file, reads the lines, and returns the resources instantly. &lt;/p&gt;




&lt;h3&gt;
  
  
  Seamless GraalVM Integration
&lt;/h3&gt;

&lt;p&gt;Of course, recording these files isn't enough; GraalVM needs to build these resources and reflected classes into the binary. &lt;code&gt;solon-aot&lt;/code&gt; coordinates this.&lt;/p&gt;

&lt;p&gt;During AOT compilation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;SolonAotProcessor&lt;/code&gt; invokes &lt;code&gt;addResourceConfig(metadata)&lt;/code&gt;. It automatically registers standard directories (&lt;code&gt;static/.*&lt;/code&gt;, &lt;code&gt;templates/.*&lt;/code&gt;, &lt;code&gt;META-INF/.*&lt;/code&gt;) and saves the pre-scanned resource paths to &lt;code&gt;solon-resource.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It generates &lt;code&gt;resource-config.json&lt;/code&gt; listing the generated &lt;code&gt;.index&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;It generates &lt;code&gt;reflect-config.json&lt;/code&gt; registering the default constructors of all components recorded in &lt;code&gt;scan_clz.index&lt;/code&gt; so GraalVM doesn't strip them away.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Solon Native Customization: &lt;code&gt;RuntimeNativeRegistrar&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Sometimes, third-party libraries perform dynamic reflections or resources scans that Solon's automated AOT processor cannot intercept.&lt;/p&gt;

&lt;p&gt;For these cases, Solon provides the &lt;code&gt;RuntimeNativeRegistrar&lt;/code&gt; interface. You can declare a custom component to manually register resources or reflections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyNativeRegistrar&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RuntimeNativeRegistrar&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RuntimeNativeMetadata&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Manually register some resources&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerResourceInclude&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-custom-config.xml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Manually register classes for serialization&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerSerialization&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MyDataTransferObject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Register class for reflection (constructors + methods)&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerReflection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MyLegacyService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
            &lt;span class="nc"&gt;MemberCategory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INVOKE_DECLARED_CONSTRUCTORS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
            &lt;span class="nc"&gt;MemberCategory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INVOKE_DECLARED_METHODS&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This bean is automatically collected during AOT compilation and injected into the GraalVM metadata compilation suite.&lt;/p&gt;




&lt;h3&gt;
  
  
  Summary: Performance Benefits
&lt;/h3&gt;

&lt;p&gt;By substituting runtime Classpath/JAR scanning with compile-time index generation, Solon achieves:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Near-Zero Scanning Overhead&lt;/strong&gt;: No CPU cycles are wasted traversing JAR directories during startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraalVM native compatibility&lt;/strong&gt;: Seamlessly translates classpath wildcard scans (&lt;code&gt;classpath:com/demo/**/*.class&lt;/code&gt;) into predictable, static lookups that satisfy GraalVM's closed-world restrictions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incredibly Fast Startup&lt;/strong&gt;: Solon applications typically boot in &lt;strong&gt;0.1 to 0.2 seconds&lt;/strong&gt; on standard JVMs, and under &lt;strong&gt;2 to 5 milliseconds&lt;/strong&gt; as compiled GraalVM native binaries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Solon's AOT compiler demonstrates that achieving GraalVM native support doesn't require rewriting your code or producing messy generated classes—sometimes, a simple index is all you need to keep things clean, simple, and blazing fast.&lt;/p&gt;

</description>
      <category>java</category>
      <category>solon</category>
      <category>graalvm</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
