<?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>Giving an AI Agent a Real Sandbox: Filesystem and Network Jail, in Java</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 14 Sep 2026 00:53:49 +0000</pubDate>
      <link>https://dev.to/solonjava/giving-an-ai-agent-a-real-sandbox-filesystem-and-network-jail-in-java-28bi</link>
      <guid>https://dev.to/solonjava/giving-an-ai-agent-a-real-sandbox-filesystem-and-network-jail-in-java-28bi</guid>
      <description>&lt;p&gt;Ask a coding agent to run a build, and you have just handed a language model the ability to &lt;code&gt;cat ~/.ssh/id_rsa&lt;/code&gt;. Prompt-level instructions like "do not read sensitive files" are not a security boundary — they are a suggestion to a stochastic process. If the agent executes commands on your machine, the only control that actually holds is the one the operating system enforces.&lt;/p&gt;

&lt;p&gt;That is the problem &lt;a href="https://solon.noear.org/article/learn-solon-ai" rel="noopener noreferrer"&gt;Solon AI&lt;/a&gt;'s new &lt;code&gt;solon-ai-sandbox&lt;/code&gt; module solves. It is a Java port of Claude Code's &lt;code&gt;sandbox-runtime&lt;/code&gt;, and it wraps agent-issued commands in real filesystem and network isolation — on macOS, Linux, and Windows.&lt;/p&gt;

&lt;p&gt;All code below was verified against the &lt;code&gt;solon-ai-sandbox&lt;/code&gt; source in the Solon AI 4.1.x tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just run the agent in Docker?
&lt;/h2&gt;

&lt;p&gt;Containers are the usual answer, and for a server-side agent they are the right one. But the agents people actually run interactively — the ones editing their working copy of a repo — are not in a container. They are on a laptop, in a terminal, one &lt;code&gt;bash&lt;/code&gt; call away from everything the user can touch.&lt;/p&gt;

&lt;p&gt;Booting a VM or a container per command is too slow for that loop, and it breaks the agent's access to the working tree you wanted it to edit. What you want is a &lt;em&gt;narrow&lt;/em&gt; boundary: keep the agent in the project directory, let it reach the registries and package mirrors the build needs, and make everything else fail closed — without a container runtime in the picture.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;solon-ai-sandbox&lt;/code&gt; does exactly that, using each platform's native facility:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sandbox-exec&lt;/code&gt; with a generated Seatbelt profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bubblewrap&lt;/code&gt; (&lt;code&gt;bwrap&lt;/code&gt;), plus &lt;code&gt;socat&lt;/code&gt; for the network bridge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;srt-win.exe&lt;/code&gt; with a WFP filter layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The module depends on nothing but &lt;code&gt;solon-ai-core&lt;/code&gt;, so pulling it in does not drag a container runtime along with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The entry point is one class
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SandboxManager&lt;/code&gt; is a final class with static methods — there is one sandbox per process, and there is one place to configure it.&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-sandbox&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;${solon-ai.version}&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;Initialization takes a runtime config and an optional interactive callback:&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;SandboxManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;askCallback&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And wrapping a command is a single call:&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;String&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SandboxManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wrapWithSandbox&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"git status"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Process&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRuntime&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;exec&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;String&lt;/span&gt;&lt;span class="o"&gt;[]{&lt;/span&gt;&lt;span class="s"&gt;"/bin/bash"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS the result is a &lt;code&gt;sandbox-exec -p '&amp;lt;seatbelt profile&amp;gt;'&lt;/code&gt; invocation; on Linux it is a &lt;code&gt;bwrap&lt;/code&gt; invocation with the appropriate bind mounts and namespaces. Your code never branches on the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filesystem: two different policies, on purpose
&lt;/h2&gt;

&lt;p&gt;Reads and writes use opposite defaults, and understanding why is the key to configuring this correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writes are &lt;code&gt;allow-only&lt;/code&gt;.&lt;/strong&gt; The default is &lt;em&gt;deny everything&lt;/em&gt;. You list the paths the agent may write, and &lt;code&gt;denyWrite&lt;/code&gt; punches holes back out of that list. The manager always adds the paths a process genuinely cannot function without — &lt;code&gt;/dev/*&lt;/code&gt;, temp directories, and so on — so you are not fighting the OS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reads are &lt;code&gt;deny-then-allow-back&lt;/code&gt;.&lt;/strong&gt; The default is &lt;em&gt;allow&lt;/em&gt;, because breaking every read on the machine would break the compiler, the JVM, and half of userspace. Instead you name the regions to protect, and &lt;code&gt;allowRead&lt;/code&gt; re-opens specific paths inside them.&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;FilesystemConfig&lt;/span&gt; &lt;span class="n"&gt;fs&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;FilesystemConfig&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="s"&gt;"~/.ssh"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"~/.aws"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// denyRead&lt;/span&gt;
    &lt;span class="nc"&gt;Collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;emptyList&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;             &lt;span class="c1"&gt;// allowRead&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="s"&gt;"/tmp"&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;// allowWrite&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="s"&gt;".git"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;               &lt;span class="c1"&gt;// denyWrite&lt;/span&gt;
    &lt;span class="kc"&gt;false&lt;/span&gt;                                &lt;span class="c1"&gt;// allowGitConfig&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that as: the agent may write only under the current working directory and &lt;code&gt;/tmp&lt;/code&gt;, must never write into &lt;code&gt;.git&lt;/code&gt;, and may not read your SSH or AWS credentials even though reads are otherwise open.&lt;/p&gt;

&lt;p&gt;Two details that matter in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An empty &lt;code&gt;allowWrite&lt;/code&gt; list is the strictest possible setting&lt;/strong&gt; — it means no writes at all beyond the mandatory system paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't write into &lt;code&gt;.git&lt;/code&gt;.&lt;/strong&gt; A corrupted or maliciously rewritten Git directory is a nasty persistence vector, which is why &lt;code&gt;denyWrite&lt;/code&gt; on &lt;code&gt;.git&lt;/code&gt; shows up in the security defaults, and why &lt;code&gt;.git/config&lt;/code&gt; gets its own &lt;code&gt;allowGitConfig&lt;/code&gt; switch (default &lt;code&gt;false&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Network: a proxy, not a firewall rule
&lt;/h2&gt;

&lt;p&gt;Network isolation here is implemented with a local HTTP and SOCKS5 forward proxy. The sandboxed process is pointed at it via environment variables, and the module decides per request whether to let the connection through.&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;NetworkConfig&lt;/span&gt; &lt;span class="n"&gt;network&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;NetworkConfig&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="s"&gt;"api.openai.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*.github.com"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// allowlist&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="s"&gt;"telemetry.example.com"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;// denylist&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="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="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="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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Domain patterns support wildcards like &lt;code&gt;*.github.com&lt;/code&gt;, and &lt;code&gt;HostUtils&lt;/code&gt; normalizes IPv4, IPv6, and hostnames so that matching is not trivially bypassed by writing an address a different way.&lt;/p&gt;

&lt;p&gt;The reason to use a proxy instead of a kernel firewall rule is &lt;strong&gt;live updates&lt;/strong&gt;. The proxies read the configuration on every request, so this takes effect immediately, on already-running agent processes, with no rebind and no port change:&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;SandboxManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newConfig&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that with filesystem rules, which are &lt;em&gt;not&lt;/em&gt; live: on macOS the rules are baked into the Seatbelt profile when the command is wrapped, and on Windows they have to be explicitly re-stamped. To change filesystem restrictions you must &lt;code&gt;reset()&lt;/code&gt; and &lt;code&gt;initialize()&lt;/code&gt; again. Know which knob is hot and which one requires a restart.&lt;/p&gt;

&lt;p&gt;There is also a callback for the case where the allowlist is not the final word:&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;SandboxAskCallback&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hostPattern&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="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;"Allow "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hostPattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHost&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="n"&gt;hostPattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPort&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"? [y/N]"&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;Scanner&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;in&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;nextLine&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="na"&gt;equalsIgnoreCase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"y"&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;It fails closed: if the callback throws, or no configuration covers the request, the connection is denied.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constructor signature changed — watch out for this
&lt;/h2&gt;

&lt;p&gt;Here is a concrete trap. The module's README still shows a &lt;code&gt;SandboxRuntimeConfig&lt;/code&gt; with &lt;strong&gt;12&lt;/strong&gt; constructor arguments. The current source declares &lt;strong&gt;13&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;SandboxRuntimeConfig&lt;/span&gt; &lt;span class="n"&gt;config&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;SandboxRuntimeConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// NetworkConfig&lt;/span&gt;
    &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                            &lt;span class="c1"&gt;// FilesystemConfig&lt;/span&gt;
    &lt;span class="n"&gt;ignoredViolations&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// Map&amp;lt;String, List&amp;lt;String&amp;gt;&amp;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;// enableWeakerNestedSandbox&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;// enableWeakerNetworkIsolation&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;// allowAppleEvents&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;// RipgrepConfig&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;// mandatoryDenySearchDepth&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;// allowPty&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;// SeccompConfig&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;// bwrapPath&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;// socatPath&lt;/span&gt;
    &lt;span class="kc"&gt;null&lt;/span&gt;                           &lt;span class="c1"&gt;// WindowsConfig  &amp;lt;-- the 13th&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trailing &lt;code&gt;WindowsConfig&lt;/code&gt; parameter is the one the README example is missing, so code copied from it will not compile against 4.1.x. I verified this directly in &lt;code&gt;SandboxRuntimeConfig.java&lt;/code&gt;; the code in this post is written against the source, not the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows needs a different call
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wrapWithSandbox(String)&lt;/code&gt; returns a shell string, and on Windows it throws instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;wrapWithSandbox() returns a shell string and is not supported on Windows. Use SandboxManager.wrapWithSandboxArgv()...&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The argv variant returns &lt;code&gt;{ argv, env }&lt;/code&gt;, where &lt;code&gt;env&lt;/code&gt; carries the full proxy environment the child needs to inherit. On macOS and Linux it still works — it just wraps the string form behind &lt;code&gt;&amp;lt;shell&amp;gt; -c&lt;/code&gt; — so if you want one code path across all three platforms, use &lt;code&gt;wrapWithSandboxArgv&lt;/code&gt; everywhere and spawn with &lt;code&gt;{ shell: false }&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability: violations are data, not just logs
&lt;/h2&gt;

&lt;p&gt;A blocked operation is an event worth recording. &lt;code&gt;SandboxViolationStore&lt;/code&gt; is a thread-safe, category-keyed store:&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;SandboxViolationStore&lt;/span&gt; &lt;span class="n"&gt;store&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;SandboxViolationStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ignoreViolations&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"network"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"attempted connection to telemetry.example.com:443"&lt;/span&gt;&lt;span class="o"&gt;);&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;category&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCategories&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="n"&gt;category&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="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getViolations&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&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 violations are categorized — &lt;code&gt;file_read&lt;/code&gt;, &lt;code&gt;file_write&lt;/code&gt;, &lt;code&gt;network&lt;/code&gt; — a burst of &lt;code&gt;network&lt;/code&gt; denials from a normally well-behaved agent is a signal worth alerting on. &lt;code&gt;ignoreViolations&lt;/code&gt; suppresses known-noisy entries by substring match, which keeps the signal readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you ship this: two operational gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Check dependencies at startup, and fail loudly.&lt;/strong&gt; &lt;code&gt;initialize()&lt;/code&gt; refuses to proceed if the platform's dependency is missing, and you should surface that rather than silently degrading to an unsandboxed run:&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;Platform&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PlatformDetector&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;detect&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;SandboxDependencyCheck&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SandboxManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;checkDependencies&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;deps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasErrors&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sandbox unavailable: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getErrors&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;On Linux that means &lt;code&gt;bubblewrap&lt;/code&gt; and &lt;code&gt;socat&lt;/code&gt; must be installed (&lt;code&gt;apt install bubblewrap socat&lt;/code&gt;); macOS needs nothing, since &lt;code&gt;sandbox-exec&lt;/code&gt; ships with the OS; Windows needs &lt;code&gt;srt-win.exe&lt;/code&gt; installed once with elevation to set up the WFP layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Call &lt;code&gt;cleanupAfterCommand()&lt;/code&gt; after each command on Linux.&lt;/strong&gt; &lt;code&gt;bwrap&lt;/code&gt; creates empty placeholder files on the &lt;em&gt;host&lt;/em&gt; filesystem when it protects paths that do not exist — &lt;code&gt;~/.bashrc&lt;/code&gt; on a fresh container, for example. They linger after the process exits. The method is a no-op on macOS, and it is also invoked from &lt;code&gt;reset()&lt;/code&gt; and a JVM shutdown hook, but calling it in your command loop avoids accumulating junk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;Three rules cover almost all of it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Writes are opt-in, reads are opt-out.&lt;/strong&gt; Configure writes as an explicit allowlist; configure reads as a short list of things worth protecting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network is hot, filesystem is cold.&lt;/strong&gt; Allowlist changes apply to running processes; filesystem changes need a reset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed, and fail visibly.&lt;/strong&gt; Missing dependencies and unmatched callbacks should surface as errors, not as an unsandboxed fallback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The larger point is about where the boundary lives. Once an agent can execute code, "the model was asked nicely" is not a control. &lt;code&gt;solon-ai-sandbox&lt;/code&gt; moves that control down into the mechanism the OS already enforces — Seatbelt, bubblewrap, or the Windows Filtering Platform — and hands you a small, uniform Java API for it.&lt;/p&gt;

&lt;p&gt;For a Java agent stack, that is the difference between a demo and something you let near a real repository.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Solon AI documentation: &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;Module source (&lt;code&gt;solon-ai-sandbox&lt;/code&gt;), Solon AI 4.1.x tree&lt;/li&gt;
&lt;li&gt;Claude Code &lt;code&gt;sandbox-runtime&lt;/code&gt; (the TypeScript original this module ports)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>java</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>MCP Without the Boilerplate: Solon AI's Annotation-Driven Server and Self-Healing Client</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Tue, 08 Sep 2026 02:23:53 +0000</pubDate>
      <link>https://dev.to/solonjava/mcp-without-the-boilerplate-solon-ais-annotation-driven-server-and-self-healing-client-hg9</link>
      <guid>https://dev.to/solonjava/mcp-without-the-boilerplate-solon-ais-annotation-driven-server-and-self-healing-client-hg9</guid>
      <description>&lt;p&gt;If you have been following this series, you have seen how Solon AI streams chat as semantic events and how it chunks documents by meaning. This time we move from conversation plumbing to capability plumbing: &lt;strong&gt;how Solon AI turns ordinary Java code into MCP (Model Context Protocol) services, and how it consumes remote MCP servers without hand-writing protocol code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All code in this post was verified against the Solon AI 4.1.x source tree (&lt;code&gt;solon-ai-mcp&lt;/code&gt; and &lt;code&gt;mcp-core&lt;/code&gt; modules).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP, and why in-process?
&lt;/h2&gt;

&lt;p&gt;MCP standardizes how an LLM application discovers and invokes &lt;strong&gt;tools&lt;/strong&gt;, reads &lt;strong&gt;resources&lt;/strong&gt;, and loads &lt;strong&gt;prompts&lt;/strong&gt; from an external provider. Instead of hard-coding function calls into your prompt pipeline, you point your app at an MCP endpoint — local or remote — and the capability list arrives over the wire.&lt;/p&gt;

&lt;p&gt;Solon AI ships MCP support in two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mcp-core&lt;/code&gt;&lt;/strong&gt; — a self-contained protocol implementation covering MCP spec revisions from &lt;code&gt;2024-11-05&lt;/code&gt; through &lt;code&gt;2025-11-25&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;solon-ai-mcp&lt;/code&gt;&lt;/strong&gt; — the application-facing layer: an annotation-driven server and a &lt;code&gt;ToolProvider&lt;/code&gt;-compatible client that plugs straight into &lt;code&gt;ChatModel&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design goal is visible in the dependency direction: the protocol layer knows nothing about Solon AI, and the integration layer adds almost nothing you have to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server: one annotation per capability
&lt;/h2&gt;

&lt;p&gt;Declare an endpoint class with &lt;code&gt;@McpServerEndpoint&lt;/code&gt;, then annotate plain methods:&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;mcpEndpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/mcp/sse"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;heartbeatInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"30s"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&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;McpServerTool&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Tip: enable the -parameters compiler flag,&lt;/span&gt;
    &lt;span class="c1"&gt;// or give every @Param an explicit name.&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;"查询天气预报"&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;"城市位置"&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;location&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;"晴，14度"&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 is the whole server. At startup, &lt;code&gt;McpPlugin&lt;/code&gt; scans &lt;code&gt;@McpServerEndpoint&lt;/code&gt; classes and builds an &lt;code&gt;McpServerEndpointProvider&lt;/code&gt; from them. Method-level providers — &lt;code&gt;MethodToolProvider&lt;/code&gt;, &lt;code&gt;MethodResourceProvider&lt;/code&gt;, &lt;code&gt;MethodPromptProvider&lt;/code&gt; — extract &lt;code&gt;@ToolMapping&lt;/code&gt;, &lt;code&gt;@ResourceMapping&lt;/code&gt;, and &lt;code&gt;@PromptMapping&lt;/code&gt; methods and register them with the endpoint's lifecycle. No JSON schemas to maintain by hand, no dispatch switch, no transport wiring.&lt;/p&gt;

&lt;p&gt;Details worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;heartbeatInterval&lt;/code&gt; defaults to &lt;code&gt;"30s"&lt;/code&gt; on the server side.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sseEndpoint()&lt;/code&gt; and &lt;code&gt;messageEndpoint()&lt;/code&gt; are deprecated; the unified &lt;code&gt;mcpEndpoint()&lt;/code&gt; is the way to go.&lt;/li&gt;
&lt;li&gt;There are two hosting models: a &lt;strong&gt;stateful&lt;/strong&gt; host (&lt;code&gt;STREAMABLE&lt;/code&gt; channel) that keeps session state per client, and a &lt;strong&gt;stateless&lt;/strong&gt; one (&lt;code&gt;STREAMABLE_STATELESS&lt;/code&gt;) where every request carries everything it needs — the better fit behind load balancers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enableOutputSchema()&lt;/code&gt; can turn on output schema validation for tools that need it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The client: &lt;code&gt;McpClientProvider&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On the consuming side, one class implements &lt;code&gt;ToolProvider&lt;/code&gt;, &lt;code&gt;ResourceProvider&lt;/code&gt;, and &lt;code&gt;PromptProvider&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;McpClientProvider&lt;/span&gt; &lt;span class="n"&gt;mcpClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;McpClientProvider&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;url&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:8081/sse"&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;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;chatConfig&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;mcpClient&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;ChatResponse&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;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;"杭州天气和北京降雨量如何？"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider is lazy: the underlying &lt;code&gt;McpAsyncClient&lt;/code&gt; is created on first use, guarded by a lock. From then on, &lt;code&gt;ChatModel&lt;/code&gt; treats MCP tools exactly like local function tools — the model sees them in its tool list, picks one, and Solon AI routes the invocation through &lt;code&gt;callTool(name, args)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four channels, one builder
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;McpChannel&lt;/code&gt; defines &lt;code&gt;STDIO&lt;/code&gt;, &lt;code&gt;SSE&lt;/code&gt;, &lt;code&gt;STREAMABLE&lt;/code&gt;, and &lt;code&gt;STREAMABLE_STATELESS&lt;/code&gt;. The builder picks the transport for you:&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;Transport&lt;/th&gt;
&lt;th&gt;Typical use&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;&lt;code&gt;StdioClientTransport&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Launching a local MCP binary as a subprocess&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;&lt;code&gt;WebRxSseClientTransport&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Classic HTTP + server-sent events&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;&lt;code&gt;WebRxStreamableHttpTransport&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Modern streamable HTTP, stateful session&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;&lt;code&gt;WebRxStreamableHttpTransport&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stateless streamable HTTP, LB-friendly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For per-call granularity instead of defaults, push the tool list into the prompt options:&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;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;"今天杭州的天气情况？"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toolAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTools&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;filter&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;-&amp;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;is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TEXT_DELTA&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;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasText&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ChatEvent:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getText&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note how this composes with the ChatEvent streaming API from the first post in this series — MCP tools and semantic events are orthogonal layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-healing connections
&lt;/h3&gt;

&lt;p&gt;Network transport code fails in boring, repetitive ways. &lt;code&gt;McpClientProvider&lt;/code&gt; centralizes the retry:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;T&lt;/span&gt; &lt;span class="nf"&gt;executeWithRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;McpAsyncClient&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;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="k"&gt;try&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;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getClient&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;block&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;ex&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;isTransportError&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reset&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                                 &lt;span class="c1"&gt;// drop the broken client&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getClient&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// reconnect, retry once&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;ex&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;isTransportError&lt;/code&gt; matches &lt;code&gt;McpTransportException&lt;/code&gt;, timeouts, connection refusals, and friends. Protocol-level errors (a tool that returned an error, for example) propagate untouched — retrying those would be wrong.&lt;/p&gt;

&lt;p&gt;If you enable heartbeats, a failed beat doubles the backoff interval on each retry, capped at 10 minutes. Intervals under 5 seconds are rejected outright. And note the asymmetry: the server sends heartbeats every 30s by default, while the client opts in explicitly — a deliberate choice to keep the client quiet unless you ask.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching and change notification
&lt;/h3&gt;

&lt;p&gt;Listing tools, resources, and prompts over MCP is a round trip you do not want on every prompt. The client caches these lists locally for 30 seconds by default (&lt;code&gt;cacheSeconds&lt;/code&gt;). When the server emits a change notification, the matching cache entry is invalidated — the next listing goes back over the wire. The notification clears the cache; it does not push the new list. Subtle, but it is the difference between "eventually fresh" and "push-updated," and it keeps the client simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool allow-lists and deny-lists
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;allowedTools&lt;/code&gt; and &lt;code&gt;disallowedTools&lt;/code&gt; filter what the client exposes. Filtering applies the allow-list first, then the deny-list — so a tool must pass both gates to reach your model. Handy for exposing a curated subset of a large third-party MCP server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration-driven wiring
&lt;/h3&gt;

&lt;p&gt;Instead of building clients in code, bind them from configuration under the &lt;code&gt;solon.ai.mcp.client.&amp;lt;name&amp;gt;&lt;/code&gt; prefix:&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;McpClientProvider&lt;/span&gt; &lt;span class="nf"&gt;clientWrapper&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${solon.ai.mcp.client.demo}"&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;client&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;client&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;There is also &lt;code&gt;McpClientProviders.fromMcpServers(uri)&lt;/code&gt; for loading a whole &lt;code&gt;mcpServers&lt;/code&gt;-style map at once — useful when your tool landscape lives in config rather than Java.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas I hit while reading the source
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Javadoc lies a little.&lt;/strong&gt; The class-level example in &lt;code&gt;McpClientProvider&lt;/code&gt; shows &lt;code&gt;.apiUrl(...)&lt;/code&gt; and &lt;code&gt;.defaultToolsAdd(...)&lt;/code&gt;; the actual builder method is &lt;code&gt;.url(...)&lt;/code&gt;, and the demo code uses &lt;code&gt;defaultToolAdd&lt;/code&gt;. Trust the code, not the comment — I have reported the drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-parameters&lt;/code&gt; matters.&lt;/strong&gt; Without the compiler flag, parameter names vanish from bytecode, and &lt;code&gt;@Param&lt;/code&gt; needs an explicit &lt;code&gt;name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat defaults are asymmetric.&lt;/strong&gt; Server: 30s on. Client: off. Do not assume both ends keep-alive the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless is a mode, not a transport.&lt;/strong&gt; &lt;code&gt;STREAMABLE_STATELESS&lt;/code&gt; uses the same HTTP transport; the difference is in session handling on the server host.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When does this matter?
&lt;/h2&gt;

&lt;p&gt;The annotation server shines when you have an existing Solon service full of business methods that AI agents suddenly need to call. The client shines when you want to compose capabilities across process boundaries — a weather server here, a database MCP there, all flowing into one &lt;code&gt;ChatModel&lt;/code&gt; with retries and caching you did not write.&lt;/p&gt;

&lt;p&gt;Together with the streaming events and semantic splitting covered earlier, that completes a picture worth remembering: Solon AI treats MCP not as a bolt-on integration but as another expression of the same builder-and-provider abstractions the rest of the framework runs on.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Solon AI repository and 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 specification: &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Earlier in this series: &lt;a href="https://dev.to/solonjava/beyond-token-streaming-solon-ai-41s-semantic-chat-events-5g8o"&gt;Semantic Chat Events&lt;/a&gt;, &lt;a href="https://dev.to/solonjava/chunk-by-meaning-not-just-size-a-deep-dive-into-solon-ais-semanticsplitter-49c3"&gt;SemanticSplitter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Verified against the Solon AI 4.1.x source tree; class and method names reflect that version.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>java</category>
      <category>mcp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Chunk by Meaning, Not Just Size: A Deep Dive into Solon AI's SemanticSplitter</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 07 Sep 2026 06:07:19 +0000</pubDate>
      <link>https://dev.to/solonjava/chunk-by-meaning-not-just-size-a-deep-dive-into-solon-ais-semanticsplitter-49c3</link>
      <guid>https://dev.to/solonjava/chunk-by-meaning-not-just-size-a-deep-dive-into-solon-ais-semanticsplitter-49c3</guid>
      <description>&lt;h1&gt;
  
  
  Chunk by Meaning, Not Just Size: A Deep Dive into Solon AI’s SemanticSplitter
&lt;/h1&gt;

&lt;p&gt;RAG quality is often discussed as if the only question were which vector database to choose. In practice, the shape of the text entering that database matters just as much. If one chunk mixes a refund rule, a shipping exception, and an account-security note, a strong embedding model cannot fully repair the damaged boundary.&lt;/p&gt;

&lt;p&gt;Solon AI’s &lt;code&gt;SemanticSplitter&lt;/code&gt; offers a different trade-off from regex- and token-based splitting: it uses an embedding model to look for likely topic boundaries before applying a token-size fallback.&lt;/p&gt;

&lt;p&gt;This article examines what the implementation actually does in Solon AI &lt;code&gt;v4.1.0&lt;/code&gt;, where it fits in the RAG pipeline, and which assumptions an application should still validate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The splitter is an ingestion-stage component
&lt;/h2&gt;

&lt;p&gt;Solon’s RAG documentation separates the main stages clearly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DocumentLoader -&amp;gt; DocumentSplitter -&amp;gt; RepositoryStorable.save(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DocumentLoader&lt;/code&gt; turns a file or other source into &lt;code&gt;Document&lt;/code&gt; objects. A &lt;code&gt;DocumentSplitter&lt;/code&gt; can then turn large documents into smaller retrieval units. The resulting documents are finally stored in a &lt;code&gt;Repository&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SemanticSplitter&lt;/code&gt; belongs to the middle stage. It does not query a vector database and it does not decide which documents to retrieve at runtime. Its job is to decide where the boundaries of the documents should be before indexing.&lt;/p&gt;

&lt;p&gt;The class is:&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;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;noear&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;splitter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SemanticSplitter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it implements the same &lt;code&gt;DocumentSplitter&lt;/code&gt; abstraction used by other Solon AI splitters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the default constructor configures
&lt;/h2&gt;

&lt;p&gt;The simplest construction is:&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;SemanticSplitter&lt;/span&gt; &lt;span class="n"&gt;splitter&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;SemanticSplitter&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source defines these defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&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;similarityThreshold&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A lower similarity marks a candidate boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxChunkTokenSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;512&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Target maximum size for a semantic group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;similarityWindow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of preceding sentences used as context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minSentencesPerChunk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum distance between accepted boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delimiters&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ALL_COMMON_DELIM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Common English, Chinese, and newline delimiters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are defaults, not universal production settings. Similarity distributions vary between embedding models, languages, and corpora. A threshold that is useful for one model can be too aggressive or too conservative for another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The algorithm is a sliding-window comparison
&lt;/h2&gt;

&lt;p&gt;The implementation does not embed the entire document once and then run a generic clustering algorithm. Its flow is more specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raw text
  -&amp;gt; sentence-like segments
  -&amp;gt; sliding context windows
  -&amp;gt; embeddings for windows and following sentences
  -&amp;gt; cosine similarities
  -&amp;gt; threshold-based boundaries
  -&amp;gt; token-size fallback
  -&amp;gt; new Documents with inherited metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Sentence-like segmentation
&lt;/h3&gt;

&lt;p&gt;The splitter scans the text using the configured delimiters. The delimiter remains attached to the preceding segment. The built-in common set includes:&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;SemanticSplitter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ALL_COMMON_DELIM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which covers &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, Chinese &lt;code&gt;。&lt;/code&gt;, &lt;code&gt;！&lt;/code&gt;, &lt;code&gt;？&lt;/code&gt;, &lt;code&gt;；&lt;/code&gt;, and newlines.&lt;/p&gt;

&lt;p&gt;This is deliberately simpler than a full natural-language sentence parser. If a document uses unusual punctuation or has long sections without the configured delimiters, the resulting segments will reflect that.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build a context window
&lt;/h3&gt;

&lt;p&gt;For each position, the splitter concatenates the next &lt;code&gt;similarityWindow&lt;/code&gt; sentences into a window. With the default window of three, the comparison looks conceptually like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[sentence 0 + sentence 1 + sentence 2]  vs  [sentence 3]
[sentence 1 + sentence 2 + sentence 3]  vs  [sentence 4]
[sentence 2 + sentence 3 + sentence 4]  vs  [sentence 5]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each side is embedded. The cosine similarity between the window and the following sentence becomes the signal for that position.&lt;/p&gt;

&lt;p&gt;A low score suggests that the next sentence may represent a topic transition. It is a candidate boundary, not a semantic truth guarantee.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Apply the threshold
&lt;/h3&gt;

&lt;p&gt;The source uses the direction below:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similarities&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;similarityThreshold&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// candidate split&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This direction matters. Holding the text, model, and other parameters constant, increasing the threshold makes it easier for a position to fall below the threshold, so it will usually create more candidate boundaries. Lowering the threshold usually keeps larger groups together.&lt;/p&gt;

&lt;p&gt;The result is not a simple linear knob. The final chunks also depend on the embedding model, the window size, the minimum-sentence rule, and the token fallback.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Enforce a token-size fallback
&lt;/h3&gt;

&lt;p&gt;After semantic groups are created, the splitter counts tokens using its configured jtokkit encoding. If a group is over &lt;code&gt;maxChunkTokenSize&lt;/code&gt;, it allocates sentences into smaller groups until the group would exceed the limit.&lt;/p&gt;

&lt;p&gt;That makes the token limit a second-stage guardrail, not the primary boundary detector.&lt;/p&gt;

&lt;p&gt;There is an important edge case: the fallback only moves whole sentences. If one individual sentence is already larger than the configured maximum, it can still appear as an oversized output chunk. Also, very short documents with no more than &lt;code&gt;similarityWindow&lt;/code&gt; segments are returned as one document before the normal embedding and token fallback path runs.&lt;/p&gt;

&lt;p&gt;So the safe statement is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;SemanticSplitter&lt;/code&gt; applies a token-size fallback to oversized semantic groups, but applications should still validate final chunk sizes for short documents and individually oversized sentences.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Embedding cost is part of the design
&lt;/h2&gt;

&lt;p&gt;Regex splitting and token splitting can be performed locally. Semantic splitting calls the configured &lt;code&gt;EmbeddingModel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a document with &lt;code&gt;N&lt;/code&gt; segments and a window size of &lt;code&gt;W&lt;/code&gt;, the normal path builds approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;N - W window texts
N - W following-sentence texts
2 * (N - W) embedding inputs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inputs are sent in batches according to &lt;code&gt;embeddingModel.batchSize()&lt;/code&gt;. The current implementation also requests:&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;options&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;dimensions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That detail deserves operational attention. The selected embedding provider must support the requested dimension behavior, and the application should measure whether the chosen dimension preserves acceptable boundary quality for its corpus. Do not assume that every provider interprets dimensionality options identically.&lt;/p&gt;

&lt;p&gt;The practical cost model is therefore different:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Splitter&lt;/th&gt;
&lt;th&gt;Boundary signal&lt;/th&gt;
&lt;th&gt;Typical cost&lt;/th&gt;
&lt;th&gt;Main risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RegexTextSplitter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Document structure&lt;/td&gt;
&lt;td&gt;Local CPU&lt;/td&gt;
&lt;td&gt;Structure may be inconsistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TokenSizeTextSplitter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Token count&lt;/td&gt;
&lt;td&gt;Local CPU&lt;/td&gt;
&lt;td&gt;A chunk can cross topic boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SemanticSplitter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Embedding similarity&lt;/td&gt;
&lt;td&gt;Embedding calls plus CPU&lt;/td&gt;
&lt;td&gt;Cost and thresholds need validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a ranking in which the semantic splitter always wins. It is a choice between different failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata is preserved, but chunk identity is not invented
&lt;/h2&gt;

&lt;p&gt;When the splitter creates a new &lt;code&gt;Document&lt;/code&gt;, it passes the original metadata through:&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="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="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful for filters such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source file;&lt;/li&gt;
&lt;li&gt;department;&lt;/li&gt;
&lt;li&gt;tenant;&lt;/li&gt;
&lt;li&gt;document version;&lt;/li&gt;
&lt;li&gt;access scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the splitter does not automatically add a chunk number, parent-document ID, page number, character offsets, or neighboring-chunk links. If those fields are needed for citation, deletion, re-indexing, or audit trails, add them in the application’s ingestion layer rather than assuming they are framework-generated.&lt;/p&gt;

&lt;p&gt;A production ingestion pipeline might therefore enrich the metadata after splitting:&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;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;splitter&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;loadedDocuments&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for&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;i&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;chunks&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="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Add application-owned tracking fields here:&lt;/span&gt;
    &lt;span class="c1"&gt;// chunkIndex, sourceId, pageNumber, version, and so on.&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;chunks&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact metadata API depends on the application’s ownership model. The important design point is that chunk tracking is an application responsibility unless the loader already supplies the required fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to evaluate it instead of guessing
&lt;/h2&gt;

&lt;p&gt;Semantic chunking should be evaluated with the same discipline as a retrieval model. A useful experiment can use one mixed-topic corpus containing, for example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;refund rules;&lt;/li&gt;
&lt;li&gt;shipping compensation;&lt;/li&gt;
&lt;li&gt;account-security requirements;&lt;/li&gt;
&lt;li&gt;similar vocabulary across all three sections.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compare at least these measurements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;number of produced chunks;&lt;/li&gt;
&lt;li&gt;average and P95 token count;&lt;/li&gt;
&lt;li&gt;number of chunks crossing a known section boundary;&lt;/li&gt;
&lt;li&gt;embedding request count;&lt;/li&gt;
&lt;li&gt;ingestion latency;&lt;/li&gt;
&lt;li&gt;Recall@K on a fixed question set;&lt;/li&gt;
&lt;li&gt;retrieved-context token count;&lt;/li&gt;
&lt;li&gt;whether the final answer cites the correct section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A small parameter sweep is also more informative than copying defaults blindly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;similarityThreshold: 0.35 / 0.50 / 0.65
similarityWindow:    1 / 3 / 5
maxChunkTokenSize:   256 / 512 / 1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Report results with the corpus, embedding model, parameter values, and measurement method. Without those details, a claim such as “semantic splitting improves accuracy” is too broad to be reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Agent RAG fits
&lt;/h2&gt;

&lt;p&gt;Solon AI has separate concepts for ingestion and runtime retrieval. A useful mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SemanticSplitter
      -&amp;gt; better-shaped Documents
      -&amp;gt; Repository
      -&amp;gt; RepositoryTool
      -&amp;gt; ReActAgent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These components answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SemanticSplitter&lt;/code&gt;: where should the knowledge be divided?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Repository&lt;/code&gt;: where are the indexed documents stored and searched?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RepositoryTool&lt;/code&gt;: how can retrieval be exposed as a tool?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ReActAgent&lt;/code&gt;: when should the agent search, and whether it should search again?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Improving chunk boundaries can help the retrieval layer, but it does not turn the splitter into an agent. Conversely, an agent cannot reliably compensate for every bad ingestion boundary. Treat the ingestion strategy and runtime strategy as separate, measurable layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical decision guide
&lt;/h2&gt;

&lt;p&gt;Use a structure-based splitter when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;headings and paragraphs are reliable;&lt;/li&gt;
&lt;li&gt;indexing cost must be minimal;&lt;/li&gt;
&lt;li&gt;deterministic boundaries are important;&lt;/li&gt;
&lt;li&gt;you need straightforward debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a token splitter when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;model input limits are the dominant constraint;&lt;/li&gt;
&lt;li&gt;the corpus has weak structure;&lt;/li&gt;
&lt;li&gt;you want predictable chunk sizes;&lt;/li&gt;
&lt;li&gt;the retrieval quality trade-off is acceptable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Evaluate &lt;code&gt;SemanticSplitter&lt;/code&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the corpus contains frequent topic transitions inside long sections;&lt;/li&gt;
&lt;li&gt;structural delimiters are not enough;&lt;/li&gt;
&lt;li&gt;you can afford embedding work during ingestion;&lt;/li&gt;
&lt;li&gt;you have a representative retrieval benchmark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hybrid pipeline can also be reasonable, but its order and benefit should be tested on the target corpus rather than declared as a universal best practice.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;SemanticSplitter&lt;/code&gt; is interesting precisely because it is not magic. It is a concrete algorithm with visible costs and boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sentence-like segmentation first;&lt;/li&gt;
&lt;li&gt;sliding-window embeddings;&lt;/li&gt;
&lt;li&gt;cosine similarity as a boundary signal;&lt;/li&gt;
&lt;li&gt;threshold-based grouping;&lt;/li&gt;
&lt;li&gt;token-size fallback;&lt;/li&gt;
&lt;li&gt;metadata inheritance;&lt;/li&gt;
&lt;li&gt;no automatic chunk identity model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it possible to reason about, benchmark, and adapt. In a Solon AI RAG pipeline, the best splitter is not the one with the most sophisticated name. It is the one whose boundary behavior, embedding cost, and retrieval results are understood on your own documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/opensolon/solon-ai/blob/v4.1.0/solon-ai-core/src/main/java/org/noear/solon/ai/rag/splitter/SemanticSplitter.java" rel="noopener noreferrer"&gt;SemanticSplitter source in Solon AI v4.1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/opensolon/solon-ai/v4.1.0/solon-ai-core/src/main/java/org/noear/solon/ai/rag/splitter/SemanticSplitter.java" rel="noopener noreferrer"&gt;Raw SemanticSplitter source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://solon.noear.org/article/935" rel="noopener noreferrer"&gt;Solon AI: Document loading and splitting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://solon.noear.org/article/1075" rel="noopener noreferrer"&gt;Solon AI: Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>java</category>
      <category>opensource</category>
      <category>rag</category>
    </item>
    <item>
      <title>Beyond Token Streaming: Solon AI 4.1's Semantic Chat Events</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 07 Sep 2026 04:30:29 +0000</pubDate>
      <link>https://dev.to/solonjava/beyond-token-streaming-solon-ai-41s-semantic-chat-events-5g8o</link>
      <guid>https://dev.to/solonjava/beyond-token-streaming-solon-ai-41s-semantic-chat-events-5g8o</guid>
      <description>&lt;p&gt;Streaming an LLM response looks simple until the response contains more than text.&lt;/p&gt;

&lt;p&gt;Depending on the provider, a modern stream may include reasoning fragments, tool-call arguments, citations, media updates, safety decisions, usage snapshots, status messages, and errors over the same connection. Treating every frame as “another partial response” pushes provider-specific parsing into the UI and makes it difficult to distinguish a transient fragment from the final answer.&lt;/p&gt;

&lt;p&gt;Solon AI 4.1 addresses that problem with a semantic event stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before 4.1:       Flux&amp;lt;ChatResponse&amp;gt;
Starting with 4.1: Flux&amp;lt;ChatEvent&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change is more than a generic type replacement. &lt;code&gt;ChatEvent&lt;/code&gt; represents what is happening while a response is in flight. &lt;code&gt;ChatResponse&lt;/code&gt; represents an aggregated result. The distinction gives application code a vocabulary for text, reasoning, tools, lifecycle, usage, and failure without requiring it to understand every provider's raw SSE dialect.&lt;/p&gt;

&lt;p&gt;This article builds on the official &lt;a href="https://solon.noear.org/article/1495" rel="noopener noreferrer"&gt;streaming event guide&lt;/a&gt; and checks the behavior against the Solon AI 4.1 source and core tests. One important qualification: the event API is marked &lt;code&gt;@Preview("4.1")&lt;/code&gt; in the current source, so treat it as a preview contract that may evolve in future releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;call()&lt;/code&gt; and &lt;code&gt;stream()&lt;/code&gt; now answer different questions
&lt;/h2&gt;

&lt;p&gt;The synchronous API still asks for the completed answer:&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;ChatResponse&lt;/span&gt; &lt;span class="n"&gt;response&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;"Explain semantic streaming"&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="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="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getText&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The streaming API asks to observe the response lifecycle:&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;Flux&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;events&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;"Explain semantic streaming"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference prevents an intermediate frame from pretending to be a complete response.&lt;/p&gt;

&lt;p&gt;For a text-only projection, select the exact semantic event you need:&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;Flux&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;text&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;message&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;filter&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="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TEXT_DELTA&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;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasText&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ChatEvent:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getText&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 intentional:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;isDelta()&lt;/code&gt; is too broad for a typewriter projection. It also covers reasoning, tool arguments, media partials, and refusal deltas.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getText()&lt;/code&gt; is nullable. &lt;code&gt;hasText()&lt;/code&gt; keeps a Reactor &lt;code&gt;map&lt;/code&gt; from receiving a null result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the first practical rule of the new model: &lt;strong&gt;project by meaning, not by transport shape&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nine groups provide the current routing layer
&lt;/h2&gt;

&lt;p&gt;The current source defines 31 event types organized into nine groups:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;What it represents&lt;/th&gt;
&lt;th&gt;Representative events&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LIFECYCLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whole-response state&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RESPONSE_START&lt;/code&gt;, &lt;code&gt;STATUS&lt;/code&gt;, &lt;code&gt;HEARTBEAT&lt;/code&gt;, &lt;code&gt;RESPONSE_END&lt;/code&gt;, &lt;code&gt;ABORT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;STEP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One model round&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;STEP_START&lt;/code&gt;, &lt;code&gt;STEP_END&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TEXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User-visible answer text&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TEXT_START&lt;/code&gt;, &lt;code&gt;TEXT_DELTA&lt;/code&gt;, &lt;code&gt;TEXT_END&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;THINKING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reasoning-related output&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;THINKING_START&lt;/code&gt;, &lt;code&gt;THINKING_DELTA&lt;/code&gt;, &lt;code&gt;THINKING_END&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TOOL_CALL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client-executed tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TOOL_CALL_START&lt;/code&gt;, &lt;code&gt;TOOL_CALL_ARGS_DELTA&lt;/code&gt;, &lt;code&gt;TOOL_CALL_END&lt;/code&gt;, &lt;code&gt;TOOL_RESULT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SERVER_TOOL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provider-side tools&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SERVER_TOOL_START&lt;/code&gt;, &lt;code&gt;SERVER_TOOL_ARGS_DELTA&lt;/code&gt;, &lt;code&gt;SERVER_TOOL_RESULT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MEDIA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Citations and generated media&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CITATION&lt;/code&gt;, &lt;code&gt;MEDIA_PARTIAL&lt;/code&gt;, &lt;code&gt;MEDIA_DONE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SAFETY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refusals and filtering&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;REFUSAL_DELTA&lt;/code&gt;, &lt;code&gt;CONTENT_FILTER&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;META&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Usage, errors, raw and custom data&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;USAGE&lt;/code&gt;, &lt;code&gt;ERROR&lt;/code&gt;, &lt;code&gt;RAW&lt;/code&gt;, &lt;code&gt;CUSTOM&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These groups are useful when building a generic router. Specific event types can evolve while the application keeps stable top-level destinations:&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&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;getGroup&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;TEXT:&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;is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TEXT_DELTA&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;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasText&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appendAnswer&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;getText&lt;/span&gt;&lt;span class="o"&gt;());&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="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;THINKING:&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;is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;THINKING_DELTA&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;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasText&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appendReasoning&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;getText&lt;/span&gt;&lt;span class="o"&gt;());&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="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;TOOL_CALL:&lt;/span&gt;
            &lt;span class="n"&gt;toolPanel&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;event&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="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;SERVER_TOOL:&lt;/span&gt;
            &lt;span class="n"&gt;serverToolPanel&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;event&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="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;MEDIA:&lt;/span&gt;
            &lt;span class="n"&gt;mediaPanel&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;event&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="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;SAFETY:&lt;/span&gt;
            &lt;span class="n"&gt;safetyPanel&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;event&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="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;STEP:&lt;/span&gt;
            &lt;span class="n"&gt;timeline&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;event&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="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;LIFECYCLE:&lt;/span&gt;
            &lt;span class="n"&gt;lifecycle&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;event&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="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;META:&lt;/span&gt;
            &lt;span class="n"&gt;diagnostics&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;event&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="k"&gt;default&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;debug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unhandled event type: {}"&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;getType&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;Keeping a &lt;code&gt;default&lt;/code&gt; branch is still sensible for preview APIs and future versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  An event-phase &lt;code&gt;END&lt;/code&gt; is not necessarily the end of the stream
&lt;/h2&gt;

&lt;p&gt;Events also have phases such as &lt;code&gt;START&lt;/code&gt;, &lt;code&gt;DELTA&lt;/code&gt;, &lt;code&gt;END&lt;/code&gt;, and &lt;code&gt;NONE&lt;/code&gt;. The word &lt;code&gt;END&lt;/code&gt; is local to an event scope.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TEXT_END&lt;/code&gt; closes a text block;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;THINKING_END&lt;/code&gt; closes a reasoning block;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TOOL_CALL_END&lt;/code&gt; closes one tool call;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STEP_END&lt;/code&gt; closes one model round;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RESPONSE_END&lt;/code&gt; closes a successful response lifecycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;event.isTerminal()&lt;/code&gt; when you mean a whole-stream semantic terminal. In the current enum, it is true for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RESPONSE_END
ABORT
ERROR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not replace that check with &lt;code&gt;event.getPhase() == END&lt;/code&gt;. &lt;code&gt;ERROR&lt;/code&gt; is terminal but has phase &lt;code&gt;NONE&lt;/code&gt;, while several local boundary events have phase &lt;code&gt;END&lt;/code&gt; without terminating the response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recover the final response from &lt;code&gt;RESPONSE_END&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If a caller needs the final aggregate while using the streaming path, it should select &lt;code&gt;RESPONSE_END&lt;/code&gt; first:&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;ChatResponse&lt;/span&gt; &lt;span class="n"&gt;response&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;query&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;filter&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="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RESPONSE_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;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ChatEvent:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getResponse&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;blockFirst&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The asynchronous form can expose the first matching terminal response as a &lt;code&gt;Mono&amp;lt;ChatResponse&amp;gt;&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;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;response&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;query&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;filter&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="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;is&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RESPONSE_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;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ChatEvent:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getResponse&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;blockFirst()&lt;/code&gt; on the unfiltered stream is a classic migration mistake. The first event is normally &lt;code&gt;RESPONSE_START&lt;/code&gt;, not the final answer.&lt;/p&gt;

&lt;p&gt;The caller does not need to concatenate every &lt;code&gt;TEXT_DELTA&lt;/code&gt; to reconstruct the official final response. Solon AI accumulates the response internally and attaches the aggregate to the terminal event. Local concatenation can still be useful for immediate rendering, but it should be treated as a UI projection rather than the source of truth for tool calls, usage, and the complete assistant message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The normalizer creates consistent stateful event boundaries
&lt;/h2&gt;

&lt;p&gt;Provider stream formats differ in how consistently they expose boundaries. A provider may send a text delta without an explicit text-start frame. Tool-call fragments may omit a stable identifier after the first frame. Reasoning and answer text may alternate. A stream can terminate while a content block is still open.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ChatEventNormalizer&lt;/code&gt; sits between parsed provider semantics and the subscriber. Its job is to make event boundaries usable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TEXT_START -&amp;gt; TEXT_DELTA* -&amp;gt; TEXT_END
THINKING_START -&amp;gt; THINKING_DELTA* -&amp;gt; THINKING_END
TOOL_CALL_START -&amp;gt; TOOL_CALL_ARGS_DELTA* -&amp;gt; TOOL_CALL_END
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For text and thinking blocks, the normalizer uses stricter tracking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bare delta can cause a matching start to be synthesized;&lt;/li&gt;
&lt;li&gt;duplicate starts for the same block are dropped;&lt;/li&gt;
&lt;li&gt;an unmatched end is dropped;&lt;/li&gt;
&lt;li&gt;switching between thinking and text closes the previously open block;&lt;/li&gt;
&lt;li&gt;completion closes remaining open blocks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool calls use a deliberately looser policy. An arguments delta can synthesize a start when no call is open, and an unmatched end is preserved. This is not a malformed-stream validator. It is a compatibility layer for providers whose later argument fragments do not repeat a complete tool-call ID.&lt;/p&gt;

&lt;p&gt;That difference is important when writing application code. The normalizer provides compatibility-oriented boundary completion, not strict tool-call identity validation. Use normalized events for rendering and correlation, but read the complete &lt;code&gt;ToolCall&lt;/code&gt; objects from a completed step or final response rather than assuming that one argument delta contains valid JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool execution introduces steps
&lt;/h2&gt;

&lt;p&gt;An automatic tool workflow is not a single model round. It commonly looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RESPONSE_START
  STEP_START (0)
    TOOL_CALL_START
    TOOL_CALL_ARGS_DELTA ...
    TOOL_CALL_END
    TOOL_RESULT
  STEP_END (0)
  STEP_START (1)
    TEXT_START
    TEXT_DELTA ...
    TEXT_END
  STEP_END (1)
RESPONSE_END
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response lifecycle remains one unit, while each provider request becomes a step. The current implementation starts step numbering at zero and increments it for recursive model calls.&lt;/p&gt;

&lt;p&gt;A normally completed &lt;code&gt;STEP_END&lt;/code&gt; event carries the terminal snapshot and usage for that step. &lt;code&gt;RESPONSE_END&lt;/code&gt; carries the whole successful response aggregate and cross-step usage total. This lets an observability system answer two different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What did this model round produce and cost?&lt;/li&gt;
&lt;li&gt;What did the complete tool-assisted response produce and cost?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool argument fragments may be interleaved when multiple calls are in flight. Correlate them by &lt;code&gt;toolCallId&lt;/code&gt; when one is available; do not build a parser that assumes all fragments for one call arrive as a single contiguous JSON document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage is a two-level aggregation problem
&lt;/h2&gt;

&lt;p&gt;Usage data is easy to overcount.&lt;/p&gt;

&lt;p&gt;Within one provider step, usage frames are commonly cumulative snapshots. Adding every frame would count the same tokens repeatedly. Across steps, however, each step is a separate model request, so completed-step totals need to be added.&lt;/p&gt;

&lt;p&gt;Solon AI's stream session reflects that distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;within a step: keep/merge the provider snapshot
across steps:  add completed-step usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consequently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;STEP_END.getUsage()&lt;/code&gt; describes the current completed step;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RESPONSE_END.getUsage()&lt;/code&gt; describes the accumulated successful response;&lt;/li&gt;
&lt;li&gt;a standalone &lt;code&gt;USAGE&lt;/code&gt; event is an observation, not an instruction to blindly add every value it contains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source also preserves provider usage metadata across steps. Numeric fields such as token and server-tool counts are accumulated, while label-like values use different merge behavior. The larger lesson is portable: billing telemetry needs a scope model, not just a counter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors, aborts, and cancellation are different
&lt;/h2&gt;

&lt;p&gt;A production subscriber must distinguish three mechanisms.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ERROR&lt;/code&gt; and Reactor &lt;code&gt;onError&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;For failures that enter the main reactive error path, Solon AI attempts to expose an &lt;code&gt;ERROR&lt;/code&gt; event before terminating the Reactor stream with an error. The two channels serve different purposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the event channel can carry semantic context such as a previously completed response snapshot or accumulated usage;&lt;/li&gt;
&lt;li&gt;Reactor &lt;code&gt;onError&lt;/code&gt; drives retry, fallback, timeout, and recovery operators.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They describe one failure, not two failures. Also, do not assume that &lt;code&gt;ERROR.getResponse()&lt;/code&gt; contains the partially streamed text from the currently failing step. The aggregate may contain only previously completed steps, or it may be null when the first step fails early.&lt;/p&gt;

&lt;p&gt;A custom event filter can suppress a &lt;code&gt;META&lt;/code&gt; event such as &lt;code&gt;ERROR&lt;/code&gt;, while Reactor &lt;code&gt;onError&lt;/code&gt; still arrives. Keep an error consumer even if the UI also handles error events:&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;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;query&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="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;route&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;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;recoverOrReport&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;markTransportComplete&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;ABORT&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ABORT&lt;/code&gt; is an upstream semantic event. It is not the same thing as the subscriber cancelling its subscription. Application code can route it as a lifecycle signal, but should not infer Reactor cancellation from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reactor cancellation
&lt;/h3&gt;

&lt;p&gt;Operators such as &lt;code&gt;take(...)&lt;/code&gt; may cancel the subscription. Once cancellation occurs, the downstream cannot expect extra events. In particular, it should not wait for synthetic &lt;code&gt;TEXT_END&lt;/code&gt;, &lt;code&gt;STEP_END&lt;/code&gt;, &lt;code&gt;ABORT&lt;/code&gt;, or &lt;code&gt;RESPONSE_END&lt;/code&gt; events after it has cancelled.&lt;/p&gt;

&lt;p&gt;Cleanup belongs in Reactor lifecycle hooks such as &lt;code&gt;doFinally&lt;/code&gt;, not in a handler that assumes every path ends with &lt;code&gt;RESPONSE_END&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="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;query&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;doFinally&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;releaseUiResources&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&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;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;recoverOrReport&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Event filtering is delivery policy, not aggregation policy
&lt;/h2&gt;

&lt;p&gt;The default filter rejects high-volume &lt;code&gt;HEARTBEAT&lt;/code&gt; and unmodeled &lt;code&gt;RAW&lt;/code&gt; events. Diagnostic or gateway code can request more:&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;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;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;eventFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or it can extend the default policy:&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;ChatEventFilter&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatEventFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEFAULT&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;or&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;ChatEventFilter&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="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RAW&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;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;eventFilter&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="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a subtle source-level detail worth knowing. Runtime filtering is guarded so lifecycle and step groups survive a non-null custom filter. Therefore, this:&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eventFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventFilter&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="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TEXT_DELTA&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does &lt;strong&gt;not&lt;/strong&gt; mean the subscriber will receive only text deltas. Lifecycle and step events remain available for aggregation and boundaries. In the current preview implementation, the interaction between the default filter, guarded custom filters, and &lt;code&gt;HEARTBEAT&lt;/code&gt; is nuanced because heartbeat itself belongs to the lifecycle group.&lt;/p&gt;

&lt;p&gt;The robust approach is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;eventFilter&lt;/code&gt; to control broad delivery volume;&lt;/li&gt;
&lt;li&gt;still use an exact downstream &lt;code&gt;filter&lt;/code&gt; when projecting one semantic channel;&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;ChatEventFilter.all()&lt;/code&gt; only when raw protocol visibility is genuinely needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Filtering happens after Solon AI's internal normalization and aggregation. Hiding an event from a subscriber does not undo the internal final response.&lt;/p&gt;

&lt;h2&gt;
  
  
  A provider dialect translates frames; it should not duplicate semantics
&lt;/h2&gt;

&lt;p&gt;A custom provider integration implements the streaming parser entry point:&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;parseResponseJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatStreamContext&lt;/span&gt; &lt;span class="n"&gt;ctx&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;respJson&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually, there are two output paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put primary content such as answer text, reasoning, and client tool calls into the accumulator so the core can generate standard events and aggregates.&lt;/li&gt;
&lt;li&gt;Emit semantic events directly for provider capabilities such as citations, status, server-side tools, or specialized media.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following is illustrative pseudocode; &lt;code&gt;readProviderResponseId&lt;/code&gt;, &lt;code&gt;readTextDelta&lt;/code&gt;, &lt;code&gt;readCitation&lt;/code&gt;, and &lt;code&gt;buildCitation&lt;/code&gt; are provider-specific helpers:&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;@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;parseResponseJson&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatStreamContext&lt;/span&gt; &lt;span class="n"&gt;ctx&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;json&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;setProviderResponseId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readProviderResponseId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&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="n"&gt;readTextDelta&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&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="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;text&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;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccumulator&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addContentItem&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;AssistantMessage&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="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;citationUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readCitation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&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;citationUrl&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;ctx&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;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatEventType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CITATION&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buildCitation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;citationUrl&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not send the same semantic payload through both paths. If text is added to the accumulator and also emitted as a text delta, consumers can see duplicate deltas and the aggregate can be corrupted.&lt;/p&gt;

&lt;p&gt;The unified layer is valuable precisely because provider frames and application events are not one-to-one. A single frame may update usage and text; one semantic tool call may span many frames. The dialect should translate provider protocol, while the core owns the cross-provider event contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical migration checklist
&lt;/h2&gt;

&lt;p&gt;When moving pre-4.1 streaming code to the preview event model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;Flux&amp;lt;ChatResponse&amp;gt;&lt;/code&gt; declarations with &lt;code&gt;Flux&amp;lt;ChatEvent&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Render answer text only from &lt;code&gt;TEXT_DELTA&lt;/code&gt; plus &lt;code&gt;hasText()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Route reasoning separately from answer text.&lt;/li&gt;
&lt;li&gt;Do not use &lt;code&gt;isDelta()&lt;/code&gt; as a synonym for user-visible text.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;RESPONSE_END&lt;/code&gt; before calling &lt;code&gt;blockFirst()&lt;/code&gt; or &lt;code&gt;next()&lt;/code&gt; for the final response.&lt;/li&gt;
&lt;li&gt;Read completed tool calls from &lt;code&gt;STEP_END&lt;/code&gt; or &lt;code&gt;RESPONSE_END&lt;/code&gt;, not from one arguments fragment.&lt;/li&gt;
&lt;li&gt;Treat &lt;code&gt;STEP_END&lt;/code&gt; usage and &lt;code&gt;RESPONSE_END&lt;/code&gt; usage as different scopes.&lt;/li&gt;
&lt;li&gt;Keep Reactor error handling even when consuming &lt;code&gt;ERROR&lt;/code&gt; events.&lt;/li&gt;
&lt;li&gt;Treat cancellation as a transport/control action, not as &lt;code&gt;ABORT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Revisit custom dialects for the &lt;code&gt;parseResponseJson(ChatStreamContext, String)&lt;/code&gt; contract.&lt;/li&gt;
&lt;li&gt;Re-check the exact 4.1 preview API before upgrading future versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the tests establish
&lt;/h2&gt;

&lt;p&gt;The core source contains focused tests for the event model. I ran these three deterministic suites against the reviewed checkout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ChatEventNormalizerTest       20 tests
ChatEventFilterTest            6 tests
ChatStreamSessionUsageTest     9 tests
---------------------------------------
Total                         35 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tests run: 35, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They provide a useful regression net for boundary normalization, filter composition, and cross-step usage aggregation. They do not prove every arbitrary event sequence, every provider's behavior, deep immutability of every payload, or every cancellation race. Provider capabilities also differ: an application must not expect every dialect to emit reasoning, citations, media, safety, server-tool, or usage events.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architectural payoff
&lt;/h2&gt;

&lt;p&gt;The most useful part of Solon AI 4.1's streaming redesign is not the number of event types. It is the separation of responsibilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider SSE / JSON frames
          ↓
provider dialect parsing
          ↓
semantic accumulation and event emission
          ↓
boundary normalization
          ↓
application routing and protocol adapters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A UI can focus on rendering. An agent can focus on tool steps. An observability layer can focus on usage and failure. A gateway can opt into raw events without forcing ordinary consumers to pay that cost.&lt;/p&gt;

&lt;p&gt;Token streaming answers, “What bytes arrived next?” Semantic streaming answers, “What happened next?” For applications that combine reasoning, tools, citations, safety, and multiple provider calls, the semantic model is generally easier to extend.&lt;/p&gt;

&lt;p&gt;Further reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://solon.noear.org/article/1495" rel="noopener noreferrer"&gt;Official Solon AI streaming event guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/opensolon/solon-ai" rel="noopener noreferrer"&gt;Solon AI source repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>ai</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>What SolonCode's Profile Backup Really Protects</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sun, 06 Sep 2026 06:25:32 +0000</pubDate>
      <link>https://dev.to/solonjava/what-soloncodes-profile-backup-really-protects-39hk</link>
      <guid>https://dev.to/solonjava/what-soloncodes-profile-backup-really-protects-39hk</guid>
      <description>&lt;p&gt;A configuration backup is not automatically a secure backup. In an AI coding environment, the archive may contain model endpoints, tool permissions, mount definitions, local instructions, memories, scripts, and visual themes. It may also contain credentials, even when its interface says that secrets are excluded.&lt;/p&gt;

&lt;p&gt;SolonCode's Profile backup feature is a useful case study because the implementation is small enough to read end to end. It defines a portable ZIP format, separates settings from directory assets, provides a preview step, protects some settings values by default, and adds limits against oversized uploads and excessive decompression. At the same time, it deliberately stops short of encryption, authenticity, transactional restore, and general-purpose secret discovery.&lt;/p&gt;

&lt;p&gt;That combination is more interesting than a feature tour. It shows how a migration tool can establish practical boundaries without pretending to be a vault or a disaster-recovery system. This article reads the implementation as an engineering design: what it includes, what it does when restoring, and what operators must still do themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the asset model
&lt;/h2&gt;

&lt;p&gt;The service exposes six selectable backup keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;settings
skills
agents
commands
memory
skins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wording “six asset types” is convenient, but the filesystem model is more precise: one configuration item plus five directory-based asset collections. The source of truth is &lt;code&gt;ProfileService.java&lt;/code&gt;, especially &lt;code&gt;buildManifest()&lt;/code&gt;, &lt;code&gt;exportZip()&lt;/code&gt;, and &lt;code&gt;importCommit()&lt;/code&gt;. The HTTP allow-list is repeated in &lt;code&gt;ProfileSettingsController.java&lt;/code&gt; as &lt;code&gt;VALID_KEYS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The default manifest marks settings, skills, agents, commands, and memory as checked. Skins are listed but not checked by default. That distinction matters: a user can migrate the visual layer, but it is not silently included in the default selection shown by the manifest/UI path.&lt;/p&gt;

&lt;p&gt;The archive layout is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;manifest.json
settings/global.json
assets/skills/**
assets/agents/**
assets/commands/**
assets/memory/**
assets/skins/**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only selected keys are written. Settings are not copied as an arbitrary byte-for-byte file. Instead, the current &lt;code&gt;AgentSettings&lt;/code&gt; object is serialized and filtered to an allow-list of top-level groups. Directory assets are traversed and copied into their corresponding archive prefixes.&lt;/p&gt;

&lt;p&gt;This is a good portability boundary. A consumer does not need to understand the whole SolonCode home directory. It can reason about a manifest, one settings fragment, and five known asset roots. It also leaves room for future schema versions without making the first format dependent on every incidental file in a user's home directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema versioning is a gate, not authenticity
&lt;/h2&gt;

&lt;p&gt;The archive declares &lt;code&gt;schemaVersion: 1&lt;/code&gt;. The exporter writes the value into &lt;code&gt;manifest.json&lt;/code&gt;; both preview and commit require a manifest and call &lt;code&gt;requireSchemaVersion()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The importer accepts a positive version no greater than the implementation's current version. A missing, non-positive, or higher version is rejected. Rejecting a future version is preferable to silently interpreting an incompatible structure as if it were known.&lt;/p&gt;

&lt;p&gt;But schema validation should not be confused with content validation or trust validation. The implementation does not verify that the manifest's item list exactly matches the ZIP entries. It does not use a manifest hash to prove that files were not changed after export. It does not use a signature to authenticate the producer. It does not establish that a package came from a particular person, machine, or release.&lt;/p&gt;

&lt;p&gt;That is a useful general lesson for portable formats: version metadata answers “which structure should I parse?” It does not answer “who produced this?” or “has this package been tampered with?” Those are separate protocol properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings are filtered by top-level group
&lt;/h2&gt;

&lt;p&gt;The settings fragment is limited to these ten top-level groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;general
permission
loop
defaultModel
models
providers
mountPools
mcpServers
apiServers
lspServers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter is implemented by &lt;code&gt;pickSettingsGroups()&lt;/code&gt; in &lt;code&gt;ProfileService.java&lt;/code&gt;. Unknown or otherwise unlisted top-level content does not become part of the exported settings fragment. This makes the archive more intentional than a raw dump of an object graph.&lt;/p&gt;

&lt;p&gt;The restore operation has an important, sometimes surprising, matching rule. &lt;code&gt;importSettingsFile()&lt;/code&gt; iterates through the same group list and executes the equivalent of:&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;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fragment&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;group&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a group is present, the group is replaced as a whole. This is merge-by-group, not merge-by-entry. If the archive contains a &lt;code&gt;models&lt;/code&gt; object, the current models group is replaced by the archived models object; it is not merged model by model. If the archive does not contain &lt;code&gt;mcpServers&lt;/code&gt;, the current group remains untouched.&lt;/p&gt;

&lt;p&gt;This is a coherent design for a user-selected migration tool. It makes selection meaningful and avoids a vague recursive merge policy. It also creates an operational requirement: review the preview carefully before importing a group such as &lt;code&gt;models&lt;/code&gt;, &lt;code&gt;providers&lt;/code&gt;, &lt;code&gt;mountPools&lt;/code&gt;, or &lt;code&gt;lspServers&lt;/code&gt;. A group-level replacement can remove entries that exist in the current group but not in the archive.&lt;/p&gt;

&lt;p&gt;The service does create a manual recovery point before changing an existing settings file. If the current file exists, it is copied to a sibling named like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;settings.json.bak-YYYYMMDDHHMMSS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result includes a warning naming that backup. This is valuable, but it is not an automatic rollback mechanism. The backup is for &lt;code&gt;settings.json&lt;/code&gt;; it does not snapshot the five asset directories. The new settings content is written with &lt;code&gt;Files.write()&lt;/code&gt; inside the profile service rather than through the atomic temporary-file-and-move path used elsewhere by settings persistence.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Secrets excluded” has a narrow meaning
&lt;/h2&gt;

&lt;p&gt;The default export passes &lt;code&gt;includeSecrets=false&lt;/code&gt;. The implementation then recursively walks the settings JSON and masks only six exact, case-sensitive field names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiKey
api_key
webAuthPass
webAuthUser
dbPassword
ldapAdminPassword
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A value is replaced only when the matching field's value is a string. The replacement marker is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;__MASKED__
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a generic password detector. A field called &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;accessToken&lt;/code&gt;, or &lt;code&gt;clientSecret&lt;/code&gt; is not automatically recognized. A differently capitalized field is not recognized. A non-string value is not replaced by this branch. The safe wording is therefore “six exact settings field names are masked,” not “all secrets are removed.”&lt;/p&gt;

&lt;p&gt;The masking routine does recurse through objects and arrays, which means a matching name can be found below the top level. That improves coverage for nested model or server configuration. It does not change the name-based nature of the policy.&lt;/p&gt;

&lt;p&gt;There is a second boundary that is even more important: assets are not scanned. &lt;code&gt;addAssetDir()&lt;/code&gt; copies files from skills, agents, commands, memory, and skins directly into the ZIP. It does not call &lt;code&gt;maskSecrets()&lt;/code&gt;. It does not search for &lt;code&gt;.env&lt;/code&gt; files, token patterns, passwords, or the six field names inside text files.&lt;/p&gt;

&lt;p&gt;As a result, a default export can still contain a credential embedded in a skill instruction, an agent configuration, a command script, a memory note, or a skin asset. The &lt;code&gt;masked&lt;/code&gt; flag attached to the settings manifest item describes the settings fragment; it does not certify that the complete archive is secret-free.&lt;/p&gt;

&lt;p&gt;This is a strong example of why security documentation must name the protected surface. “Default secret masking” sounds broad until the implementation is read. “Default masking of six exact string field names in settings JSON; directory assets are copied without scanning” is less promotional and much more useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explicit plaintext export is a deliberate risk switch
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;includeSecrets&lt;/code&gt; is exposed by the export controller and defaults to false. If an operator explicitly sets it to true, the settings fragment retains the matching values in plaintext. The manifest records that choice, but the import path does not turn it into an additional trust or cryptographic policy.&lt;/p&gt;

&lt;p&gt;There is no ZIP encryption, no password-based key derivation, and no secret-specific encryption layer. Therefore an &lt;code&gt;includeSecrets=true&lt;/code&gt; archive should be treated as a credential bundle. It should not be committed to a source repository, attached to a public issue, placed in a shared chat, or stored on an untrusted file share.&lt;/p&gt;

&lt;p&gt;Even a default masked archive deserves review because the five directory asset types are not scanned. If the intended migration includes secrets, a better operational pattern is to keep the archive non-secret and provision credentials separately through the destination environment. If plaintext export is unavoidable, restrict access, shorten retention, and rotate exposed credentials when the transfer is complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview and commit are separate uploads
&lt;/h2&gt;

&lt;p&gt;The web controller exposes two POST endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/web/settings/profile/import/parse
/web/settings/profile/import/commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both require a multipart &lt;code&gt;file&lt;/code&gt;. Both read the complete stream into memory. Both independently invoke the profile service. The preview endpoint unpacks into a temporary directory, checks the manifest and schema, calculates settings group information, and counts new versus overwritable asset files. It does not write the target settings or asset directories.&lt;/p&gt;

&lt;p&gt;The commit endpoint receives another upload, creates another temporary directory, unpacks again, validates again, then applies the selected settings and assets.&lt;/p&gt;

&lt;p&gt;This means the preview result is not a server-side handle to an immutable package. There is no persisted review token, no archive hash comparison, and no server-held staging directory reused by commit. A user can preview archive A and submit archive B. The selected keys can also differ between the requests.&lt;/p&gt;

&lt;p&gt;The two-step UI is still useful as a human workflow, but its protocol meaning must be stated accurately: preview is an inspection operation, not a cryptographic binding between the reviewed bytes and the committed bytes. If “what I reviewed is exactly what I commit” is a requirement, the protocol needs an archive digest or a server-side staged object with an expiring commit token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource limits help, but they are not a complete ZIP policy
&lt;/h2&gt;

&lt;p&gt;The controller limits each uploaded compressed stream to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;64 * 1024 * 1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is 64 MiB. The check is in &lt;code&gt;ProfileSettingsController.readAll()&lt;/code&gt; and applies independently to the parse and commit uploads. The exporter itself builds a complete ZIP in memory and does not apply this same output limit.&lt;/p&gt;

&lt;p&gt;During extraction, &lt;code&gt;ProfileService.unzipSafely()&lt;/code&gt; counts bytes actually written across entries. If the cumulative uncompressed output exceeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;256 * 1024 * 1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that is 256 MiB, extraction stops with an error. This provides a useful defense against an archive whose compressed size is small but whose expanded output is large.&lt;/p&gt;

&lt;p&gt;There are important omissions. The code does not enforce a maximum number of entries. It does not enforce a compression-ratio limit. It does not enforce a real per-file uncompressed threshold. A local variable named &lt;code&gt;written&lt;/code&gt; is incremented, and a comment refers to limiting one file, but no comparison against a per-file limit exists. The actual implemented byte defense is cumulative, not per-entry.&lt;/p&gt;

&lt;p&gt;The importer also skips, rather than rejects, entries whose names contain &lt;code&gt;..&lt;/code&gt;, and applies a normalized path containment check. This provides basic Zip Slip protection for the temporary extraction directory. It is not a complete malicious archive policy. There is no duplicate-entry uniqueness check, no signature verification, and no complete trust decision for packages from unknown sources.&lt;/p&gt;

&lt;p&gt;Both parse and commit attempt recursive cleanup in a &lt;code&gt;finally&lt;/code&gt; block. The temporary directory prefixes are different: &lt;code&gt;soloncode-profile-unpack-&lt;/code&gt; for preview and &lt;code&gt;soloncode-profile-import-&lt;/code&gt; for commit. This is good lifecycle hygiene, but cleanup can itself encounter I/O errors, and the existing service tests do not assert that successful and failing paths leave no temporary directories behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asset restore is additive and overwriting, not mirroring
&lt;/h2&gt;

&lt;p&gt;For each selected asset directory, &lt;code&gt;importAssetDir()&lt;/code&gt; walks the extracted source and copies every file to the target with &lt;code&gt;REPLACE_EXISTING&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The resulting behavior is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a missing destination file is added;&lt;/li&gt;
&lt;li&gt;a same-path destination file is overwritten;&lt;/li&gt;
&lt;li&gt;a destination file absent from the archive is retained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third rule is easy to miss. The importer does not delete the target directory and does not synchronize it to an exact snapshot. This reduces destructive surprises, especially when a user imports only one collection. It also means migration may leave obsolete skills, commands, memories, or skin files in place.&lt;/p&gt;

&lt;p&gt;The distinction between “restore a portable selection” and “replace the complete local state” is a general design choice worth making explicit. The former is safer for ordinary users. The latter would require a much stronger review and deletion model.&lt;/p&gt;

&lt;p&gt;The asset operation is also not backed by an asset-level pre-import snapshot. If a file is overwritten and a later operation fails, the profile service does not automatically restore the old asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commit is not transactional
&lt;/h2&gt;

&lt;p&gt;The commit sequence is ordered: unpack and validate, apply settings if selected, then process skills, agents, commands, memory, and skins. There is no transaction spanning these filesystem operations, no staging tree swapped into place as one unit, and no compensating rollback.&lt;/p&gt;

&lt;p&gt;Suppose settings has already been written and several skill files have been copied when an agents file fails. The previous changes remain. The settings backup may allow manual restoration of the old settings file, but it does not restore overwritten assets and does not reverse the whole operation.&lt;/p&gt;

&lt;p&gt;This is not necessarily a defect for a compact local utility. Transactional filesystem migration is complex, especially when multiple directories and user-created files are involved. It is, however, a boundary that must appear in runbooks. Treat commit as a sequence of applied writes, not as an all-or-nothing change.&lt;/p&gt;

&lt;p&gt;A cautious migration procedure is therefore straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make an independent backup of the relevant SolonCode home data.&lt;/li&gt;
&lt;li&gt;Inspect the archive source and contents before uploading it.&lt;/li&gt;
&lt;li&gt;Preview the exact keys that will be submitted.&lt;/li&gt;
&lt;li&gt;Prefer a separate credential provisioning step.&lt;/li&gt;
&lt;li&gt;Commit in a controlled environment, possibly in smaller selections.&lt;/li&gt;
&lt;li&gt;Verify settings and representative files after commit.&lt;/li&gt;
&lt;li&gt;Reload or restart according to the changed configuration.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reload is not the same as restart
&lt;/h2&gt;

&lt;p&gt;The profile commit itself does not call reload. The controller's response and comments point callers to &lt;code&gt;/web/settings/reload&lt;/code&gt; afterward.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WebSettingsController.settingsReload()&lt;/code&gt; invokes &lt;code&gt;AgentSettings.reloadInPlace()&lt;/code&gt;. The settings class first loads and parses a disk snapshot, fills runtime defaults, compares content, and only then copies values into the current instance. The tests for &lt;code&gt;AgentSettingsReloadTest.java&lt;/code&gt; cover useful in-place properties such as retaining final group object identity, replacing maps in place, preserving memory on corrupt input, and layering local settings over global settings.&lt;/p&gt;

&lt;p&gt;The reload controller applies or diffs several categories, including general settings, permissions, default model, models, MCP servers, and API servers. But the implementation explicitly warns about two categories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mountPools changed; memory updated, restart recommended for full runtime effect
lspServers changed; memory updated, restart recommended for full runtime effect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, &lt;code&gt;mountPools&lt;/code&gt; and &lt;code&gt;lspServers&lt;/code&gt; may be updated in memory while their complete runtime effect still requires a restart. Also, reload with &lt;code&gt;apply=false&lt;/code&gt; refreshes memory without applying engine-side changes.&lt;/p&gt;

&lt;p&gt;A migration runbook should not say “import, and everything is hot.” It should say “import, reload where appropriate, and restart when the runtime warning requires it.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What the current tests prove—and do not prove
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ProfileServiceTest.java&lt;/code&gt; contains 12 tests. They were also run against the reviewed source commit with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn &lt;span class="nt"&gt;-pl&lt;/span&gt; soloncode-cli &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-Dtest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ProfileServiceTest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-Dsurefire&lt;/span&gt;.failIfNoSpecifiedTests&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run completed with 12 tests, zero failures, zero errors, zero skipped, and Maven &lt;code&gt;BUILD SUCCESS&lt;/code&gt;. Reading the assertions shows a focused baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selected-key export is checked;&lt;/li&gt;
&lt;li&gt;default masking and explicit plaintext export are checked for &lt;code&gt;apiKey&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;basic manifest structure and schema version are checked;&lt;/li&gt;
&lt;li&gt;empty selection is rejected;&lt;/li&gt;
&lt;li&gt;missing manifests and invalid schema cases are rejected;&lt;/li&gt;
&lt;li&gt;commit independently validates schema;&lt;/li&gt;
&lt;li&gt;an existing masked &lt;code&gt;apiKey&lt;/code&gt; is preserved during settings import;&lt;/li&gt;
&lt;li&gt;a deleted skill file can be restored;&lt;/li&gt;
&lt;li&gt;one &lt;code&gt;../&lt;/code&gt; Zip Slip entry is skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is useful coverage of the central happy paths. It is not complete coverage of the six-key feature or the security boundary.&lt;/p&gt;

&lt;p&gt;The tests do not directly cover the controller's 64 MiB upload limit. They do not exercise the 256 MiB cumulative extraction boundary. They do not verify temporary directory cleanup. They do not verify same-name asset overwrite together with retention of extra destination files. They only incidentally execute settings backup logic without asserting the backup file's existence, content, or warning.&lt;/p&gt;

&lt;p&gt;They also do not construct a partial failure to demonstrate the non-transactional result. They do not test preview A followed by commit B. Only &lt;code&gt;apiKey&lt;/code&gt; is tested among the six exact secret names. There is no asset file containing a secret to demonstrate that assets are copied without scanning. Agents, commands, memory, and skins lack the same file-level positive restore coverage given to skills.&lt;/p&gt;

&lt;p&gt;The test suite also cannot cover controls that the implementation does not have: entry-count limits, compression-ratio limits, per-file extraction limits, encryption, signatures, or cryptographic hashes.&lt;/p&gt;

&lt;p&gt;The right conclusion is neither “the feature is untested” nor “the feature is fully secure.” The source and tests together show a sensible baseline with clearly identifiable follow-up work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The engineering lesson
&lt;/h2&gt;

&lt;p&gt;SolonCode's Profile backup design is valuable precisely because its guarantees are bounded. It defines a portable schema v1 ZIP. It distinguishes settings from five asset directories. It filters settings by known top-level groups. It masks six exact string field names by default. It makes plaintext export explicit. It limits compressed uploads to 64 MiB and cumulative extraction to 256 MiB. It performs basic Zip Slip path checks and attempts temporary-directory cleanup. It restores assets by adding and replacing files without deleting extra destination files.&lt;/p&gt;

&lt;p&gt;It does not encrypt archives. It does not authenticate their source. It does not hash or sign their contents. It does not scan directory assets for secrets. It does not bind preview bytes to commit bytes. It does not provide entry-count, compression-ratio, or per-file limits. It does not make restore transactional or automatically roll back. It may require a restart for the complete runtime effect of &lt;code&gt;mountPools&lt;/code&gt; or &lt;code&gt;lspServers&lt;/code&gt; changes.&lt;/p&gt;

&lt;p&gt;That is a credible migration utility boundary, not a secret manager boundary. The practical design principle is simple: make the portable unit explicit, make destructive behavior limited, and document every guarantee narrowly enough that operators can build the missing controls around it.&lt;/p&gt;

&lt;p&gt;For any tool that moves AI configuration, this is the standard worth aiming for. Security is not the number of safeguards listed in a settings page. It is the precision of the contract between what the code protects, what it leaves untouched, and what the person operating the migration must still verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source paths
&lt;/h2&gt;

&lt;p&gt;The implementation and tests discussed here are in the SolonCode repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/service/ProfileService.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/settings/ProfileSettingsController.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/WebSettingsController.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/config/AgentSettings.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/test/java/org/noear/solon/codecli/portal/web/service/ProfileServiceTest.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/test/java/org/noear/solon/codecli/config/AgentSettingsReloadTest.java&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source claims in this article were checked against repository commit &lt;code&gt;334284123ca6c7069cc58f24e044f8785bd6dc9d&lt;/code&gt;. The focused &lt;code&gt;ProfileServiceTest&lt;/code&gt; run passed as described above; no performance measurement is being claimed.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>opensource</category>
      <category>java</category>
    </item>
    <item>
      <title>File Events Are Hints: Keeping SolonCode's Workspace Tree in Sync</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sat, 05 Sep 2026 11:04:19 +0000</pubDate>
      <link>https://dev.to/solonjava/file-events-are-hints-keeping-soloncodes-workspace-tree-in-sync-1hc6</link>
      <guid>https://dev.to/solonjava/file-events-are-hints-keeping-soloncodes-workspace-tree-in-sync-1hc6</guid>
      <description>&lt;p&gt;A file tree looks like a read-only view of a directory. In a coding agent, it is closer to a live distributed projection.&lt;/p&gt;

&lt;p&gt;The filesystem changes outside the browser. A Java watcher observes platform events. A WebSocket transports them. JavaScript updates a partially expanded DOM. Meanwhile, the user can collapse a directory, reconnect the page, switch workspaces, or ask the agent to create a whole package tree in one turn.&lt;/p&gt;

&lt;p&gt;That means a file event is useful, but it is not the file tree itself. It is a hint that helps the UI move toward the current filesystem state.&lt;/p&gt;

&lt;p&gt;SolonCode's public source provides a compact case study in that distinction. Its workspace tree combines recursive watching, change coalescing, incremental DOM patches, lazy reconciliation, and reconnect-time refresh. The interesting part is not any single function. It is the boundary between the event stream and the authoritative directory scan.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08to1nx8yx59gq2uczpm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08to1nx8yx59gq2uczpm.png" alt="SolonCode Web workspace tree showing a clean demo workspace in the English interface" width="280" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The real SolonCode Web file panel, captured from a clean demo workspace. The screenshot is cropped to the file panel and contains no model, endpoint, account, session, or local absolute path.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This article follows the public repository at commit &lt;a href="https://github.com/opensolon/soloncode/commit/58c19665d72531c37ff127cb2855e740eedcc040" rel="noopener noreferrer"&gt;&lt;code&gt;58c19665&lt;/code&gt;&lt;/a&gt;. That snapshot is later than the latest formal release verified while writing, &lt;code&gt;v2026.8.30&lt;/code&gt;, so treat this as a source analysis rather than a guarantee about every installed binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The synchronization pipeline
&lt;/h2&gt;

&lt;p&gt;At a high level, the path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;filesystem
  -&amp;gt; JDK WatchService
  -&amp;gt; ChangeEntry(wsId, path, kind, type)
  -&amp;gt; net-effect merge by workspace + path
  -&amp;gt; system.filer_change over WebGate
  -&amp;gt; onFilerChange(payload)
  -&amp;gt; incremental DOM update or lazy reconciliation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend and frontend have different responsibilities.&lt;/p&gt;

&lt;p&gt;The backend knows which physical root changed and converts platform events into a small transport contract. The frontend knows which parts of the tree are currently rendered, expanded, collapsed, or absent. Neither side alone owns the complete user-visible state.&lt;/p&gt;

&lt;p&gt;That division is important. Sending the whole tree after every save would be simple but expensive and disruptive. Sending only raw events would be cheap but would force the browser to pretend that it has a complete, perfectly ordered log. SolonCode takes a hybrid approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  One watcher, multiple roots
&lt;/h2&gt;

&lt;p&gt;A SolonCode workspace can expose more than its launch directory. It can also include enabled mounts. In &lt;code&gt;WorkspaceManager.doCreateWorkspaceContext()&lt;/code&gt;, each workspace context creates its own &lt;code&gt;FileWatchService&lt;/code&gt;, registers the launch workspace as the &lt;code&gt;workspace&lt;/code&gt; root, and then registers enabled mounts.&lt;/p&gt;

&lt;p&gt;The root ID travels with every change:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChangeEntry&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;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;wsId&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;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;path&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;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;kind&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;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;type&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 prevents a path such as &lt;code&gt;src/App.java&lt;/code&gt; in one mount from being confused with the same relative path in another.&lt;/p&gt;

&lt;p&gt;The mount type also determines the downstream action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;FILES&lt;/code&gt; mount broadcasts file changes to the Web UI;&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;SKILLS&lt;/code&gt; mount refreshes the corresponding Skill group;&lt;/li&gt;
&lt;li&gt;an &lt;code&gt;AGENTS&lt;/code&gt; mount refreshes the matching agent definition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the watcher is shared infrastructure, but its handlers preserve domain boundaries. Not every filesystem event is a file-tree event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recursive registration is part of correctness
&lt;/h2&gt;

&lt;p&gt;JDK &lt;code&gt;WatchService&lt;/code&gt; registers directories, not an abstract recursive tree. SolonCode's &lt;code&gt;registerTree()&lt;/code&gt; walks the existing directory hierarchy and registers each relevant directory for create, delete, and modify events.&lt;/p&gt;

&lt;p&gt;The second half is easy to miss: when &lt;code&gt;pollEvents()&lt;/code&gt; observes a newly created directory, it calls &lt;code&gt;registerTree()&lt;/code&gt; for that new subtree. Without that step, the UI might show the new directory but never hear about files created inside it later.&lt;/p&gt;

&lt;p&gt;The watcher also excludes noisy or irrelevant directories such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.git
node_modules
target
build
.gradle
.mvn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not merely a performance tweak. A dependency installation or build can produce thousands of events that do not belong in the coding agent's navigational tree. Filtering them at the watcher boundary reduces transport noise and avoids unnecessary DOM work.&lt;/p&gt;

&lt;p&gt;The public test suite covers both sides of this behavior: changes inside a newly created nested directory are detected, while changes under an excluded &lt;code&gt;.git&lt;/code&gt; directory are not dispatched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coalesce operations into a net effect
&lt;/h2&gt;

&lt;p&gt;A single human action rarely maps to one low-level filesystem event. Saving a file may create, modify, replace, or delete temporary paths. Generators can emit bursts. Editors may use an atomic-write pattern.&lt;/p&gt;

&lt;p&gt;SolonCode does not put every observation directly onto the WebSocket. Pending changes are keyed by &lt;code&gt;workspace ID + relative path&lt;/code&gt;, and &lt;code&gt;mergeChange()&lt;/code&gt; reduces repeated operations to a structural net effect.&lt;/p&gt;

&lt;p&gt;The verified rules include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create + modify -&amp;gt; create
create + delete -&amp;gt; no event
delete + create -&amp;gt; create
modify + delete -&amp;gt; delete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This small state machine matters more than a generic "debounce" label.&lt;/p&gt;

&lt;p&gt;Imagine that &lt;code&gt;src/NewFile.java&lt;/code&gt; is created and immediately modified before the current batch is delivered. The tree only needs to learn that a file now exists. It does not need two DOM operations. If a temporary file is created and deleted within the same pending window, the correct structural outcome is no node at all.&lt;/p&gt;

&lt;p&gt;The implementation uses concurrent compare-and-replace operations in &lt;code&gt;putChange()&lt;/code&gt;, so updates to the same key are merged without turning the entire pending map into one coarse lock.&lt;/p&gt;

&lt;p&gt;After collection, &lt;code&gt;flushChanges()&lt;/code&gt; groups entries by &lt;code&gt;wsId&lt;/code&gt; and dispatches each root's list only to that root's handlers. A failing handler is caught so it does not prevent another handler from receiving its batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The event envelope is intentionally small
&lt;/h2&gt;

&lt;p&gt;The browser receives a SAEP-style event named &lt;code&gt;system.filer_change&lt;/code&gt;. Its useful shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system.filer_change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1716153600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"wsId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/Foo.java"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1716153600000&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event carries identity and intent, not file content and not a serialized tree.&lt;/p&gt;

&lt;p&gt;That keeps the hot path small. It also makes the contract honest: the browser is told that a path was observed as created, deleted, or modified. It is not told that the message is an immutable transaction record or a complete filesystem snapshot.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app-streaming.js&lt;/code&gt; treats this as a system-level event without a chat session ID and routes the payload to &lt;code&gt;onFilerChange()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply structural changes in dependency order
&lt;/h2&gt;

&lt;p&gt;The frontend sorts each batch before touching the DOM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;delete;&lt;/li&gt;
&lt;li&gt;modify;&lt;/li&gt;
&lt;li&gt;create.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Depth breaks ties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deletes run deepest path first;&lt;/li&gt;
&lt;li&gt;creates run shallowest path first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a simple dependency rule. If a directory subtree disappears, removing children before parents avoids trying to operate through a parent that is already gone. If a new subtree appears, creating parents before descendants gives later operations somewhere to attach.&lt;/p&gt;

&lt;p&gt;It also helps rename-like sequences. &lt;code&gt;WatchService&lt;/code&gt; does not give the browser a domain-level rename command here; the visible effect can arrive as delete plus create. Processing deletions first reduces path conflicts in the same batch.&lt;/p&gt;

&lt;p&gt;The ordering is not a claim that every operating system emits identical events. It is a defensive frontend rule for the events that do arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patch visible nodes; mark invisible branches dirty
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;applyFilerChange()&lt;/code&gt; separates content changes from structural changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;delete&lt;/code&gt; calls &lt;code&gt;removeTreeNode()&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create&lt;/code&gt; calls &lt;code&gt;ensureTreeNode()&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;modify&lt;/code&gt; normally does nothing to the structure;&lt;/li&gt;
&lt;li&gt;a modify event can still create a missing file node for compatibility with older or incomplete event shapes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key optimization appears when the parent directory is collapsed.&lt;/p&gt;

&lt;p&gt;A collapsed branch does not need an immediate full child list. In fact, the browser may not have rendered those children at all. Instead of fetching and painting invisible nodes, SolonCode marks the parent with &lt;code&gt;data-dirty="1"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the user expands that directory later, the click handler sees the dirty marker, requests the real one-level directory listing, and replaces that branch from the authoritative scan.&lt;/p&gt;

&lt;p&gt;This creates two update modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;visible expanded branch -&amp;gt; patch now
collapsed/unrendered branch -&amp;gt; mark dirty, reconcile on expansion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a useful general pattern for large lazy trees. Event processing stays proportional to what the user can currently see, while a later read repairs uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Insert without redrawing siblings
&lt;/h2&gt;

&lt;p&gt;When the parent container is visible, &lt;code&gt;insertNodeSorted()&lt;/code&gt; builds one new node and places it among existing siblings.&lt;/p&gt;

&lt;p&gt;The ordering matches the backend tree service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;directories before files;&lt;/li&gt;
&lt;li&gt;names compared case-insensitively within each group.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The function also checks for an existing node before insertion. That makes repeated or compatibility events harmless at the DOM level and, more importantly, leaves existing nodes untouched.&lt;/p&gt;

&lt;p&gt;Leaving siblings untouched preserves their bound handlers and current UI state. A full &lt;code&gt;innerHTML&lt;/code&gt; replacement might produce the same names but still collapse directories, reset focus, lose hover state, or move the scroll position.&lt;/p&gt;

&lt;p&gt;This concern is visible in the project's history. The &lt;a href="https://github.com/opensolon/soloncode/releases/tag/v2026.7.13" rel="noopener noreferrer"&gt;&lt;code&gt;v2026.7.13&lt;/code&gt;&lt;/a&gt; notes explicitly describe replacing refresh-driven behavior with dynamic deletion and addition because expanded file-tree nodes could collapse after a refresh. The implementation commit &lt;a href="https://github.com/opensolon/soloncode/commit/14e6510fdc2782c74a187b70009634e821506843" rel="noopener noreferrer"&gt;&lt;code&gt;14e6510&lt;/code&gt;&lt;/a&gt; changes the Java watcher, the frontend tree code, and the watcher tests together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation still needs an authoritative read
&lt;/h2&gt;

&lt;p&gt;Incremental events are the fast path, not the only path.&lt;/p&gt;

&lt;p&gt;When the WebSocket reconnects, &lt;code&gt;connectWebGate().onopen&lt;/code&gt; calls &lt;code&gt;loadTree()&lt;/code&gt;. If a tree already exists, the call enters &lt;code&gt;smartRefreshRoot()&lt;/code&gt; instead of treating the panel as an empty first load.&lt;/p&gt;

&lt;p&gt;The refresh process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;records expanded workspaces and directory paths;&lt;/li&gt;
&lt;li&gt;reloads the workspace-root list;&lt;/li&gt;
&lt;li&gt;rebuilds the visible roots;&lt;/li&gt;
&lt;li&gt;restores expanded workspaces;&lt;/li&gt;
&lt;li&gt;sorts saved paths by depth;&lt;/li&gt;
&lt;li&gt;reloads expanded directories serially, parent before child.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The directory requests are authoritative reads through the file-tree HTTP endpoint. This repairs the projection after a connection gap without pretending that the browser received every missed event.&lt;/p&gt;

&lt;p&gt;Serial restoration is deliberate. A child node cannot be found until its parent directory has been loaded into the DOM. Depth sorting establishes the dependency order, and serial requests make that order explicit.&lt;/p&gt;

&lt;p&gt;This is the larger design lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Events provide low-latency hints. Reads provide truth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A robust live tree usually needs both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the current public source does not claim
&lt;/h2&gt;

&lt;p&gt;It is useful to separate implemented mechanisms from possible hardening work.&lt;/p&gt;

&lt;p&gt;In the public source snapshot analyzed here, &lt;code&gt;smartRefreshRoot()&lt;/code&gt; clears the live tree and then rebuilds it. The code does preserve expanded paths, but it does not implement a complete detached-tree build followed by one atomic swap.&lt;/p&gt;

&lt;p&gt;The same snapshot does not show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a refresh generation token that rejects stale async results;&lt;/li&gt;
&lt;li&gt;a structural fingerprint that skips an unchanged DOM commit;&lt;/li&gt;
&lt;li&gt;explicit &lt;code&gt;scrollTop&lt;/code&gt; and &lt;code&gt;scrollLeft&lt;/code&gt; restoration;&lt;/li&gt;
&lt;li&gt;a whole-refresh watchdog;&lt;/li&gt;
&lt;li&gt;per-request timeout handling inside the file-tree module;&lt;/li&gt;
&lt;li&gt;a frontend JavaScript test suite for refresh races.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not hidden claims or implied features. They are useful review questions for the next reliability pass.&lt;/p&gt;

&lt;p&gt;For example, once a refresh spans several requests, an older request can finish after a newer refresh has begun. A generation token can prevent stale work from committing. Building into a detached container can keep the old tree visible until the replacement is complete. A structural fingerprint can make a no-change refresh a zero-DOM-change operation. A watchdog can prevent a hung request chain from leaving a permanent in-flight state.&lt;/p&gt;

&lt;p&gt;The correct article about a real codebase should say where the implementation stops, not quietly turn a design wish list into product documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the semantics, not just the watcher callback
&lt;/h2&gt;

&lt;p&gt;The current Java tests provide a solid backend baseline. They cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file create, modify, and delete;&lt;/li&gt;
&lt;li&gt;changes in an existing subdirectory;&lt;/li&gt;
&lt;li&gt;automatic registration of a newly created directory;&lt;/li&gt;
&lt;li&gt;exclusion of &lt;code&gt;.git&lt;/code&gt; changes;&lt;/li&gt;
&lt;li&gt;isolation between multiple roots;&lt;/li&gt;
&lt;li&gt;multiple handlers on one root;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;system.filer_change&lt;/code&gt; JSON shape;&lt;/li&gt;
&lt;li&gt;net-effect merge rules;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ChangeEntry&lt;/code&gt; equality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fuller end-to-end matrix would add browser-level assertions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a create event inserts one correctly sorted node;&lt;/li&gt;
&lt;li&gt;a delete removes the deepest visible node first;&lt;/li&gt;
&lt;li&gt;a collapsed parent becomes dirty but is not eagerly rendered;&lt;/li&gt;
&lt;li&gt;expanding a dirty parent replaces it from an HTTP listing;&lt;/li&gt;
&lt;li&gt;reconnect refresh preserves the expanded path set;&lt;/li&gt;
&lt;li&gt;events arriving during refresh do not disappear;&lt;/li&gt;
&lt;li&gt;an unchanged refresh does not disturb scroll or selection;&lt;/li&gt;
&lt;li&gt;a failed directory request leaves a recoverable UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction matters because a watcher test can prove that Java observed a path while the browser still displays the wrong tree. Synchronization correctness crosses process and UI boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reusable design checklist
&lt;/h2&gt;

&lt;p&gt;For any live workspace tree, ask these questions:&lt;/p&gt;

&lt;h3&gt;
  
  
  Observation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are existing directories registered recursively?&lt;/li&gt;
&lt;li&gt;Are newly created directories registered too?&lt;/li&gt;
&lt;li&gt;Which generated or hidden directories are excluded?&lt;/li&gt;
&lt;li&gt;How are multiple workspace roots distinguished?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reduction
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are repeated changes merged by stable identity?&lt;/li&gt;
&lt;li&gt;What is the net effect of create/modify/delete sequences?&lt;/li&gt;
&lt;li&gt;Are temporary create-delete pairs eliminated?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Does the event carry root, relative path, kind, and node type?&lt;/li&gt;
&lt;li&gt;Is the event a hint or a promised transaction log?&lt;/li&gt;
&lt;li&gt;What happens during disconnection?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Projection
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are deletes and creates applied in dependency order?&lt;/li&gt;
&lt;li&gt;Can visible nodes be patched without replacing siblings?&lt;/li&gt;
&lt;li&gt;Are collapsed branches marked dirty and lazily reconciled?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recovery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Is there an authoritative directory-read path?&lt;/li&gt;
&lt;li&gt;Does reconnect trigger reconciliation?&lt;/li&gt;
&lt;li&gt;Are expanded paths restored parent-first?&lt;/li&gt;
&lt;li&gt;Can stale async work overwrite a newer result?&lt;/li&gt;
&lt;li&gt;Can a failed or hung refresh recover?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verification
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Do tests cover event reduction as well as event capture?&lt;/li&gt;
&lt;li&gt;Do browser tests assert DOM stability, not only final node names?&lt;/li&gt;
&lt;li&gt;Are scroll, focus, selection, and expanded state part of the contract?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tree is a projection, not a ledger
&lt;/h2&gt;

&lt;p&gt;The most useful shift is conceptual.&lt;/p&gt;

&lt;p&gt;A filesystem watcher is not the source of truth. A WebSocket is not the source of truth. The DOM is certainly not the source of truth. They are stages in a projection whose authority remains the workspace on disk.&lt;/p&gt;

&lt;p&gt;SolonCode's design reflects that reality in practical layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recursively observe relevant roots;&lt;/li&gt;
&lt;li&gt;merge noisy low-level events into structural outcomes;&lt;/li&gt;
&lt;li&gt;patch the visible tree incrementally;&lt;/li&gt;
&lt;li&gt;defer work for collapsed branches;&lt;/li&gt;
&lt;li&gt;reconcile from directory scans after reconnect;&lt;/li&gt;
&lt;li&gt;preserve expansion dependencies while rebuilding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once file events are treated as hints rather than facts, the architecture becomes easier to reason about. The fast path can stay fast, and the recovery path can remain honest.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>java</category>
    </item>
    <item>
      <title>SolonCode in CI: Headless Runs, JSONL Events, and Remote Workspace Execution</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Fri, 04 Sep 2026 21:22:57 +0000</pubDate>
      <link>https://dev.to/solonjava/soloncode-in-ci-headless-runs-jsonl-events-and-remote-workspace-execution-58bf</link>
      <guid>https://dev.to/solonjava/soloncode-in-ci-headless-runs-jsonl-events-and-remote-workspace-execution-58bf</guid>
      <description>&lt;p&gt;Interactive coding agents are designed for a person at a keyboard. CI is not. A pipeline needs a process it can start, observe, constrain, and classify without waiting for a human to approve the next tool call.&lt;/p&gt;

&lt;p&gt;SolonCode has a separate &lt;code&gt;run&lt;/code&gt; entry point for that boundary. It is a one-shot, non-interactive execution mode: give it a prompt, let the agent work to a terminal result, consume text or structured output, and use the exit code to decide what the pipeline should do next.&lt;/p&gt;

&lt;p&gt;The useful mental model is not “a smaller chat UI.” It is “an agent process with a machine-readable contract.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest useful CI invocation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"Review the changes in this pull request. Focus on security and correctness."&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Grep,Glob,Bash(git log *),Bash(git diff *)"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--disallowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Bash(rm *)"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; dontAsk &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 15 &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-budget-usd&lt;/span&gt; 2.0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; review.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command makes several choices explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;run&lt;/code&gt; does not start the interactive UI.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;json&lt;/code&gt; produces one result object for a script to parse.&lt;/li&gt;
&lt;li&gt;The tool allowlist describes what the reviewer may inspect.&lt;/li&gt;
&lt;li&gt;The command rule blocks destructive &lt;code&gt;rm&lt;/code&gt; calls while allowing selected Git reads.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dontAsk&lt;/code&gt; is appropriate for an unattended job: an operation requiring approval is not turned into a hidden prompt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max-turns&lt;/code&gt; provides a runtime bound.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max-budget-usd&lt;/code&gt; gives the result a cost threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A CI wrapper should check the process status before trusting the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; +e
soloncode run &lt;span class="s2"&gt;"Review the changes in this PR"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Grep,Glob,Bash(git diff *)"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; dontAsk &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 15 &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-budget-usd&lt;/span&gt; 2.0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; review.json
&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.error // .result'&lt;/span&gt; review.json

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;0&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"review completed"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  2&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"agent reached the maximum number of turns"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
  4&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"agent exceeded the configured budget"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"agent execution failed"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction between the exit code and the JSON body matters. A result can be valid JSON and still represent an incomplete or over-budget execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three output modes, three consumers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;soloncode run&lt;/code&gt; supports three output formats.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;text&lt;/code&gt;: a human-oriented result
&lt;/h3&gt;

&lt;p&gt;The default writes the final answer as plain text. It is useful when a shell script only needs to display a report or append it to a log, but it is not a stable envelope for downstream automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;json&lt;/code&gt;: one terminal object
&lt;/h3&gt;

&lt;p&gt;JSON is the natural choice for a job that waits for completion. The object can contain the result, error state, session ID, metrics, estimated cost, and—when a schema is supplied—&lt;code&gt;structured_output&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"List public API methods in src"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Grep,Glob"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; dontAsk &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--json-schema&lt;/span&gt; &lt;span class="s1"&gt;'{"type":"object","properties":{"methods":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"file":{"type":"string"}}}}}}'&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.structured_output.methods'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema is an output constraint, not a permission policy. Keep the two concerns separate: &lt;code&gt;json-schema&lt;/code&gt; describes what the answer should look like; tool and permission options describe what the agent may do while producing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;stream-json&lt;/code&gt;: an observable event stream
&lt;/h3&gt;

&lt;p&gt;For longer tasks, waiting silently for one final object is a poor operational experience. With &lt;code&gt;stream-json --verbose&lt;/code&gt;, SolonCode emits one JSON object per line. The event stream includes initialization, assistant text, tool calls, tool results, and a terminal result or error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"Inspect the build and explain the first failing test"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; stream-json &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee &lt;/span&gt;run.jsonl &lt;span class="se"&gt;\\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'select(.type=="result") | .result'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because each event is a line, a pipeline can retain the complete trace and independently project the part it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Observe tool calls without parsing the final prose&lt;/span&gt;
jq &lt;span class="s1"&gt;'select(.type=="assistant")
    | .message.content[]?
    | select(.type=="tool_use")
    | {name, input}'&lt;/span&gt; run.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--verbose&lt;/code&gt;, &lt;code&gt;stream-json&lt;/code&gt; emits only the final result event. That behavior is useful when the caller wants the stream-shaped terminal record but does not need intermediate activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt input: argv or stdin
&lt;/h2&gt;

&lt;p&gt;A prompt can be the positional argument or come from a redirected stdin stream. If both are present, the command-line prompt wins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;build-error.log | soloncode run &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="s2"&gt;"Analyze this build failure and identify its root cause"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more than a convenience. Keeping large or generated input out of shell interpolation avoids quoting surprises and makes it possible for a CI step to pass a report directly into the agent.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;run&lt;/code&gt; is deliberately one-shot. It does not accept the persistent JSONL input mode. If a process must remain alive and receive multiple user messages from stdin, the separate &lt;code&gt;soloncode stream&lt;/code&gt; entry point is the clearer lifecycle contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool restrictions are part of the job definition
&lt;/h2&gt;

&lt;p&gt;A prompt saying “do not change files” is not an enforcement mechanism. In unattended execution, restrictions should be represented by options.&lt;/p&gt;

&lt;p&gt;For a read-only review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"Review this repository"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Grep,Glob"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; dontAsk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a plan-only task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"Propose a migration plan for the authentication module"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a narrowly scoped automatic edit, &lt;code&gt;acceptEdits&lt;/code&gt; allows file-oriented tools while other operations remain rejected by the permission rules. It should still be combined with a specific allowlist and a turn limit.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ToolName(pattern)&lt;/code&gt; form is useful when a whole tool is too broad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Grep,Bash(git diff *),Bash(git log *)"&lt;/span&gt;
&lt;span class="nt"&gt;--disallowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Bash(rm *)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is matched as a tool command rule. This is preferable to allowing arbitrary shell execution merely because the task needs &lt;code&gt;git diff&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bound the agent in three dimensions
&lt;/h2&gt;

&lt;p&gt;A robust CI invocation usually has three independent bounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capability bound&lt;/strong&gt; — which tools and commands can be called.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work bound&lt;/strong&gt; — how many reasoning/action turns may run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost bound&lt;/strong&gt; — how much estimated usage the result may report.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;--max-turns&lt;/code&gt; is the runtime guard. &lt;code&gt;--max-budget-usd&lt;/code&gt; is currently checked after execution completes, so it should not be treated as a hard mid-run kill switch. Use both, and make the pipeline’s policy explicit for exit code 4.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--fallback-model&lt;/code&gt; can make a scheduled job more tolerant of primary-model unavailability, but it does not remove the need for bounded work and a clear output contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"Run the nightly code-health inspection"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowedTools&lt;/span&gt; &lt;span class="s2"&gt;"Read,Grep,Glob,Bash(git log *)"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; dontAsk &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 25 &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-budget-usd&lt;/span&gt; 3.0 &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--fallback-model&lt;/span&gt; haiku &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; reports/health.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two phases with a resumable session
&lt;/h2&gt;

&lt;p&gt;Some jobs are easier to reason about when analysis and modification are separate phases. The JSON result exposes a session ID that can be used by a later invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

soloncode run &lt;span class="s2"&gt;"Analyze src/auth and list the risks"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 10 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; phase1.json

&lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.session_id'&lt;/span&gt; phase1.json&lt;span class="si"&gt;)&lt;/span&gt;

soloncode run &lt;span class="s2"&gt;"Using that analysis, write unit tests for src/auth"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--resume&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$session&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; acceptEdits &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 20 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; phase2.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session is an agent conversation boundary, not a replacement for Git branches, worktrees, or artifact storage. If the second phase edits files, the CI job still needs its normal diff and test checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote execution: the same contract behind HTTP
&lt;/h2&gt;

&lt;p&gt;A local shell is not always the right integration point. A build service may already run SolonCode on a worker, or a Java application may want to submit work without installing the CLI.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/web/run&lt;/code&gt; endpoint carries the same one-shot execution contract over HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analyze this module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"output_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stream-json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_turns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowed_tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git diff *)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permission_mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dontAsk"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workspace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ci-001"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTTP representation uses &lt;code&gt;snake_case&lt;/code&gt;, while the CLI keeps its flag spelling. Unknown option fields are rejected instead of silently ignored. That failure mode is important: a typo must not make a caller believe that a safety option was applied.&lt;/p&gt;

&lt;p&gt;For streaming requests, the response is SSE. Each &lt;code&gt;data:&lt;/code&gt; line contains the same JSON event that the CLI would write as one JSONL line, so an existing event parser can be reused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:18080/web/run &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"prompt":"Analyze code quality","options":{"output_format":"stream-json"}}'&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^data:'&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^data: *//'&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'select(.type=="result") | .result'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server implementation starts a child &lt;code&gt;App run&lt;/code&gt; process in the selected workspace. That choice preserves the CLI’s argument parsing and execution semantics while keeping per-request engine options from mutating the web process’s interactive engine. It also means a request pays a JVM startup cost; the isolation and zero-drift properties are the more important trade-off for this boundary.&lt;/p&gt;

&lt;p&gt;The endpoint adds network-specific semantics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bearer authentication is required.&lt;/li&gt;
&lt;li&gt;The server accepts registered workspace identifiers, not arbitrary paths.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bypassPermissions&lt;/code&gt; is rejected for &lt;code&gt;/web/run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A session already running receives a conflict response rather than interleaving messages.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/web/run/interrupt&lt;/code&gt; returns &lt;code&gt;202&lt;/code&gt; and destroys the active child process.&lt;/li&gt;
&lt;li&gt;A client disconnect is treated as a reason to stop work, not as permission to keep an orphaned agent running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not expose this endpoint as an unauthenticated general-purpose shell. It drives an agent with file and command capabilities. Loopback binding, token authentication, workspace restrictions, permission-mode narrowing, and audit logging are security requirements, not optional deployment polish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exit codes are an API
&lt;/h2&gt;

&lt;p&gt;The local and remote forms intentionally distinguish execution conclusions from transport failures.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&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;0&lt;/td&gt;
&lt;td&gt;Completed successfully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Agent or API runtime error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Maximum turn limit reached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;No prompt was supplied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Estimated cost exceeded the configured budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For &lt;code&gt;/web/run&lt;/code&gt;, success is HTTP 200. A runtime error maps to HTTP 500, and a missing prompt to HTTP 400. Reaching the turn or budget limit remains HTTP 200 with an error state in the result, because the request was accepted and executed; the client should inspect the payload rather than blindly retrying at the HTTP layer.&lt;/p&gt;

&lt;p&gt;That distinction prevents a common automation bug: retrying an expensive, already-completed-but-incomplete agent run as if the network had failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical adoption sequence
&lt;/h2&gt;

&lt;p&gt;Start small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a read-only task locally with &lt;code&gt;json&lt;/code&gt; output.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;max-turns&lt;/code&gt; and an explicit tool allowlist.&lt;/li&gt;
&lt;li&gt;Make the CI step archive the JSON result and check the exit code.&lt;/li&gt;
&lt;li&gt;Add a schema when another program needs fields, not prose.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;stream-json&lt;/code&gt; when operators need progress or an audit trace.&lt;/li&gt;
&lt;li&gt;Split analysis and edits with &lt;code&gt;--resume&lt;/code&gt; only after the single-phase job is reliable.&lt;/li&gt;
&lt;li&gt;Move to &lt;code&gt;/web/run&lt;/code&gt; when execution must live on a service worker, and retain the same parser and policy checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The central design decision is to treat the agent invocation as a typed, bounded job. Prompts provide intent; options define capability and limits; events provide observability; exit codes provide control flow. That is what makes SolonCode useful in CI rather than merely runnable from a CI shell.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>From DOM Snapshots to Visual Regression: Building a Web Workflow with SolonCode's agent-browser Skill</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:14:41 +0000</pubDate>
      <link>https://dev.to/solonjava/from-dom-snapshots-to-visual-regression-building-a-web-workflow-with-soloncodes-agent-browser-3f1g</link>
      <guid>https://dev.to/solonjava/from-dom-snapshots-to-visual-regression-building-a-web-workflow-with-soloncodes-agent-browser-3f1g</guid>
      <description>&lt;p&gt;Browser automation becomes useful to a coding agent when it is more than a bag of clicks.&lt;/p&gt;

&lt;p&gt;A reliable workflow needs a way to discover the current page, act on the right element, verify the result, and leave evidence behind. It also needs to survive navigation, login state, dynamic DOM changes, and the uncomfortable fact that browser actions can have real side effects.&lt;/p&gt;

&lt;p&gt;SolonCode ships an &lt;code&gt;agent-browser&lt;/code&gt; skill that packages that workflow as a reusable skill asset. The interesting part is not that it can click a button. It is the discipline around the click:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;navigate to a page;&lt;/li&gt;
&lt;li&gt;take an accessibility snapshot;&lt;/li&gt;
&lt;li&gt;interact using references from that snapshot;&lt;/li&gt;
&lt;li&gt;take a fresh snapshot after the page changes;&lt;/li&gt;
&lt;li&gt;compare the before and after states;&lt;/li&gt;
&lt;li&gt;capture a screenshot or recording when visual evidence matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article follows the skill as it exists in the SolonCode source tree. It focuses on what the asset actually documents, including its security boundaries and limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The snapshot is the synchronization point
&lt;/h2&gt;

&lt;p&gt;The skill's core loop is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open
  -&amp;gt; snapshot -i
  -&amp;gt; click / fill / select
  -&amp;gt; snapshot again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent-browser snapshot -i&lt;/code&gt; returns interactive elements with references such as &lt;code&gt;@e1&lt;/code&gt;. A later command can use that reference to fill a field or click a button.&lt;/p&gt;

&lt;p&gt;The important rule is that references belong to a page state. After navigation or a DOM-changing action, the workflow takes a new snapshot instead of assuming that the old &lt;code&gt;@e1&lt;/code&gt; still means the same thing.&lt;/p&gt;

&lt;p&gt;That makes the snapshot a synchronization point between the browser and the agent. The agent does not operate on a stale mental picture of the page; it re-reads the page before continuing.&lt;/p&gt;

&lt;p&gt;A minimal form workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser open https://example.com/signup
agent-browser &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--load&lt;/span&gt; networkidle
agent-browser snapshot &lt;span class="nt"&gt;-i&lt;/span&gt;

&lt;span class="c"&gt;# Use the references returned by the snapshot.&lt;/span&gt;
agent-browser fill @e1 &lt;span class="s2"&gt;"Jane Doe"&lt;/span&gt;
agent-browser fill @e2 &lt;span class="s2"&gt;"jane@example.com"&lt;/span&gt;
agent-browser click @e3

agent-browser &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--load&lt;/span&gt; networkidle
agent-browser snapshot &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The references in this example are illustrative. They must come from the current snapshot; they are not stable selectors to hard-code across pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility snapshots are more than a locator trick
&lt;/h2&gt;

&lt;p&gt;A snapshot gives the agent a compact representation of the page's interactive surface. It can expose buttons, textboxes, links, checkboxes, and other controls without requiring the workflow author to invent CSS selectors for every page.&lt;/p&gt;

&lt;p&gt;When a page changes, the agent can compare the new tree with the previous one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser snapshot &lt;span class="nt"&gt;-i&lt;/span&gt;
agent-browser click @e2
agent-browser diff snapshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff reports additions and removals in a form similar to a text diff. This is useful for a test workflow because “the click completed” is not the same as “the expected state appeared.” A menu opening, a validation message appearing, or a result row changing can become an explicit assertion in the workflow.&lt;/p&gt;

&lt;p&gt;For machine-readable consumers, the skill also documents JSON output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser snapshot &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
agent-browser get text @e1 &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the browser step composable with scripts and other tools instead of forcing every consumer to parse terminal prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete verification workflow
&lt;/h2&gt;

&lt;p&gt;A practical read-only regression check can be structured as five phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open the test page
  -&amp;gt; wait for the page to settle
  -&amp;gt; snapshot the interactive state
  -&amp;gt; perform the smallest required action
  -&amp;gt; snapshot and diff the state
  -&amp;gt; save visual evidence if needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For visual checks, the skill supports a baseline screenshot and a later comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser screenshot baseline.png
&lt;span class="c"&gt;# perform the test action and wait for the expected state&lt;/span&gt;
agent-browser diff screenshot &lt;span class="nt"&gt;--baseline&lt;/span&gt; baseline.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The screenshot diff highlights changed pixels and reports a mismatch percentage. That does not replace semantic assertions: a timestamp, animation, or intentionally dynamic region may create a visual difference that is not a regression. The strongest workflow combines both kinds of evidence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;use a snapshot diff to verify semantic page state;&lt;/li&gt;
&lt;li&gt;use a screenshot diff to detect layout or styling changes;&lt;/li&gt;
&lt;li&gt;keep the page, viewport, and test data deterministic where possible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The skill also documents full-page screenshots, annotated screenshots, PDF capture, and WebM recording. These are useful for debugging and review, but they should be treated as evidence from a particular run, not as proof that every browser or viewport behaves identically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sessions separate browser state
&lt;/h2&gt;

&lt;p&gt;Many web tasks need cookies and local storage. The skill supports named sessions so that one task's browser state does not have to be mixed with another's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser &lt;span class="nt"&gt;--session-name&lt;/span&gt; staging open https://staging.example.com
&lt;span class="c"&gt;# ... complete a test login or other setup ...&lt;/span&gt;
agent-browser close

agent-browser &lt;span class="nt"&gt;--session-name&lt;/span&gt; staging open https://staging.example.com/dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also documents saving and loading state explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser state save auth.json
agent-browser state load auth.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those two mechanisms solve related but different problems. A named session provides a reusable browser profile; an explicit state file is convenient when a workflow needs to move or restore a captured state. Neither should be committed to a repository: cookies, local storage, and session tokens can be credentials.&lt;/p&gt;

&lt;p&gt;For credentials, the skill recommends the Auth Vault flow. Password input can be piped through stdin rather than placed in shell history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | agent-browser auth save staging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; https://staging.example.com/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--username&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--password-stdin&lt;/span&gt;
agent-browser auth login staging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example is intended for a test environment. Login state is not a license to automate destructive production actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat page content as untrusted input
&lt;/h2&gt;

&lt;p&gt;A browser agent reads content that it did not author. A page can contain text that looks like an instruction, but it is still page content. The skill documents &lt;code&gt;--content-boundaries&lt;/code&gt; to wrap page-sourced output with markers that help the model distinguish browser data from agent instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_BROWSER_CONTENT_BOUNDARIES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
agent-browser snapshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a useful separation aid, not a complete prompt-injection defense. The workflow still needs to decide which pages it trusts, which actions require approval, and which data may be sent to the model.&lt;/p&gt;

&lt;p&gt;The skill documents three opt-in controls that make those decisions more explicit:&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain allowlists
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_BROWSER_ALLOWED_DOMAINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"staging.example.com,*.staging.example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allowlist can restrict navigation and related browser connections. Dependencies such as a CDN may also need to be included deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Action policies
&lt;/h3&gt;

&lt;p&gt;A policy file can deny actions by default and allow only the operations needed by a workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"navigate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"snapshot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"click"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"scroll"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"wait"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"get"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A read-only inspection workflow should not silently inherit permission to submit forms, upload files, or trigger external side effects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output limits
&lt;/h3&gt;

&lt;p&gt;Large pages can overwhelm the model context. The skill documents &lt;code&gt;AGENT_BROWSER_MAX_OUTPUT&lt;/code&gt; as a way to cap output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_BROWSER_MAX_OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output limits are a context safeguard. They are not a data classification policy; sensitive text can still appear within the allowed output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The critical caveat: safety is opt-in
&lt;/h2&gt;

&lt;p&gt;The source skill is explicit: by default, &lt;code&gt;agent-browser&lt;/code&gt; does not restrict navigation, actions, or output. The allowlist, action policy, content boundaries, and output cap must be enabled by the environment or workflow author.&lt;/p&gt;

&lt;p&gt;That distinction matters when turning a demonstration into an engineering system. “The tool supports an allowlist” is a true statement. “The tool is allowlisted by default” would not be.&lt;/p&gt;

&lt;p&gt;A sensible rollout therefore starts with a narrow test environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use a dedicated staging account;&lt;/li&gt;
&lt;li&gt;allow only the domains needed by the test;&lt;/li&gt;
&lt;li&gt;deny destructive actions unless the test explicitly needs them;&lt;/li&gt;
&lt;li&gt;keep output bounded;&lt;/li&gt;
&lt;li&gt;save screenshots and snapshots without saving credentials;&lt;/li&gt;
&lt;li&gt;review any workflow that can submit, purchase, delete, publish, or send messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A skill asset, not just a command list
&lt;/h2&gt;

&lt;p&gt;The value of this SolonCode skill is its layered structure. &lt;code&gt;SKILL.md&lt;/code&gt; defines the operating contract and the core loop. Reference files provide deeper material for authentication, session management, proxy support, snapshot references, video recording, and profiling.&lt;/p&gt;

&lt;p&gt;That separation makes the workflow easier to review. A maintainer can inspect the short trigger and operating rules first, then audit the detailed reference that a particular task needs. It also reduces the temptation to put every browser edge case into the model's initial context.&lt;/p&gt;

&lt;p&gt;The reusable unit is therefore not “a model knows how to click.” It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workflow contract
  + browser commands
  + state and authentication guidance
  + verification steps
  + explicit safety configuration
  + evidence capture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same reason a good test helper is more valuable than a raw HTTP client: it encodes the decisions that make repeated execution dependable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this skill does not promise
&lt;/h2&gt;

&lt;p&gt;The source-backed boundaries are as important as the feature list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;snapshot references are page-state references, not permanent IDs;&lt;/li&gt;
&lt;li&gt;content boundaries help distinguish page output but do not make page content trustworthy;&lt;/li&gt;
&lt;li&gt;visual diffs need deterministic test conditions and still require interpretation;&lt;/li&gt;
&lt;li&gt;saved browser state may contain secrets;&lt;/li&gt;
&lt;li&gt;iOS automation has additional macOS, Xcode, Appium, and driver requirements;&lt;/li&gt;
&lt;li&gt;browser automation can perform real-world side effects;&lt;/li&gt;
&lt;li&gt;security controls are opt-in rather than automatic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A workflow that states these limits is more useful than one that presents browser automation as a magical, risk-free operator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop
&lt;/h2&gt;

&lt;p&gt;A robust browser workflow has a shape familiar to software engineers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;observe -&amp;gt; mutate -&amp;gt; observe -&amp;gt; compare -&amp;gt; record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SolonCode's &lt;code&gt;agent-browser&lt;/code&gt; skill turns that shape into a reusable asset. Accessibility snapshots keep actions aligned with the current DOM. Sessions preserve deliberate state. Snapshot and screenshot diffs turn “it seemed to work” into inspectable evidence. Optional domain, action, content, and output controls give teams a place to define their safety boundary.&lt;/p&gt;

&lt;p&gt;The final lesson is simple: browser automation should be reviewed like code. The clicks are only the implementation. The real asset is the contract around what the workflow may visit, what it may change, how it verifies success, and what evidence it leaves behind.&lt;/p&gt;

</description>
      <category>opensource</category>
    </item>
    <item>
      <title>From write/edit to automatic feedback: How SolonCode closes the LSP loop</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:56:25 +0000</pubDate>
      <link>https://dev.to/solonjava/from-writeedit-to-automatic-feedback-how-soloncode-closes-the-lsp-loop-3bpg</link>
      <guid>https://dev.to/solonjava/from-writeedit-to-automatic-feedback-how-soloncode-closes-the-lsp-loop-3bpg</guid>
      <description>&lt;p&gt;A coding agent can write syntactically plausible code and still leave a broken project behind.&lt;/p&gt;

&lt;p&gt;The obvious answer is to give the agent an &lt;code&gt;lsp&lt;/code&gt; tool and let the model ask for diagnostics whenever it wants. SolonCode tried that shape first. The implementation put navigation and diagnostics in one tool, but diagnostics were effectively never requested. That result is not surprising: after a write, “check whether this introduced errors” is not an optional curiosity. It is part of the write operation’s feedback loop.&lt;/p&gt;

&lt;p&gt;SolonCode’s current design makes that distinction explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;write&lt;/code&gt; and &lt;code&gt;edit&lt;/code&gt; trigger diagnostics automatically after a successful change.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read&lt;/code&gt; warms the language server asynchronously without delaying the read.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;lsp&lt;/code&gt; tool is reserved for optional navigation such as definition, references, hover, symbols, and call hierarchy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting engineering is not starting a language server. It is keeping the file, the language server, the model, and the Web UI consistent while all four observe different representations of the same change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnostics should follow a write, not a model decision
&lt;/h2&gt;

&lt;p&gt;The implementation note in the repository describes the original failure plainly: ten capabilities—nine navigation operations plus diagnostics—were exposed through one tool, and diagnostics were “never called” in practice.&lt;/p&gt;

&lt;p&gt;That led to a three-layer design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write / edit / apply_patch
    -&amp;gt; sync the file
    -&amp;gt; wait for diagnostics
    -&amp;gt; append diagnostics to the tool output

read
    -&amp;gt; warm up the language server asynchronously
    -&amp;gt; do not wait and do not change the read result

lsp
    -&amp;gt; definition / references / hover / symbols / call hierarchy ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a useful rule for agent design: feedback that is necessary to evaluate a mutation belongs on the mutation path. Exploratory information can remain an explicit tool.&lt;/p&gt;

&lt;p&gt;The separation also keeps the tool schema smaller and the model’s decision burden clearer. The model does not need to remember a second call after every edit just to discover whether the edit compiled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asynchronous part: waiting for the right diagnosis
&lt;/h2&gt;

&lt;p&gt;A file write and an LSP diagnosis do not happen at the same time. The client must synchronize the document, the server must parse it, and the server may publish one or more diagnostic notifications.&lt;/p&gt;

&lt;p&gt;SolonCode’s implementation uses &lt;code&gt;syncFile&lt;/code&gt; and &lt;code&gt;waitForDiagnostics(uri, timeoutMs)&lt;/code&gt; to connect those events. The wait path tracks the write time and expected document version. A diagnostic notification with a mismatching version is treated as stale rather than as evidence about the latest edit. A 150 ms debounce gathers bursts of notifications, and the default wait budget is 2,000 ms.&lt;/p&gt;

&lt;p&gt;That budget is deliberately finite. A language server may be cold-starting or indexing a large workspace. Blocking an agent forever is worse than returning the most recent known result and making the uncertainty visible.&lt;/p&gt;

&lt;p&gt;The trade-off is documented in the repository: the first write for a language can exceed the two-second budget during process startup and initial parsing; later writes normally have a warm server. A wait setting can be adjusted with &lt;code&gt;-Dlsp.diagnosticsWait&lt;/code&gt;, but increasing it also adds latency to every write.&lt;/p&gt;

&lt;p&gt;The important property is not that every diagnosis arrives before the tool returns. It is that the agent does not silently confuse an old diagnosis with the current edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the model’s feedback useful
&lt;/h2&gt;

&lt;p&gt;Raw LSP output is a protocol payload, not good model context. SolonCode’s &lt;code&gt;LspDiagnosticReporter&lt;/code&gt; narrows it before injection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only &lt;code&gt;ERROR&lt;/code&gt; severity is retained;&lt;/li&gt;
&lt;li&gt;each file is limited to 20 displayed errors;&lt;/li&gt;
&lt;li&gt;additional errors are summarized as &lt;code&gt;... and N more&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;positions are rendered as 1-based line and column numbers;&lt;/li&gt;
&lt;li&gt;the file is represented by a workspace-relative path where possible;&lt;/li&gt;
&lt;li&gt;the model-facing block asks it to fix the errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resulting shape is intentionally compact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LSP errors detected in this file, please fix:
&amp;lt;diagnostics file="src/main/java/example/Service.java"&amp;gt;
ERROR [18:13] incompatible types ... (javac)
&amp;lt;/diagnostics&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not just presentation polish. Without severity filtering and a cap, a language server can flood the next model turn with warnings, hints, generated-file paths, and repeated secondary messages. Automatic feedback still needs a context budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  One result, two consumers
&lt;/h2&gt;

&lt;p&gt;The same diagnostic is useful to two different consumers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the model needs concise text so it can repair the code;&lt;/li&gt;
&lt;li&gt;the Web user needs structured data so the UI can display a reliable explanation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SolonCode keeps those concerns separate. &lt;code&gt;ToolPresentationFilter&lt;/code&gt; handles the &lt;code&gt;TOOL_END&lt;/code&gt; event for &lt;code&gt;write&lt;/code&gt; and &lt;code&gt;edit&lt;/code&gt;. Before the filter replaces a write result with the written content for display, it extracts the diagnostic block into &lt;code&gt;ToolEndPayload.lsp&lt;/code&gt; and removes the model-facing diagnostic prose from the user-facing result.&lt;/p&gt;

&lt;p&gt;That ordering is a small but important correctness detail. If the filter copied &lt;code&gt;args.content&lt;/code&gt; into &lt;code&gt;result&lt;/code&gt; first, the diagnostics appended to the result could be lost before they were parsed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ToolLspInfo&lt;/code&gt; then carries a structured contract:&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="n"&gt;file&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;errorCount&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;boolean&lt;/span&gt; &lt;span class="n"&gt;truncated&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&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;ToolLspDiagnostic&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&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;boolean&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each item has a line, column, message, and optional source. The browser does not parse XML or prompt wording. It renders the contract it receives.&lt;/p&gt;

&lt;p&gt;This makes prompt tuning safe: changing the model-facing sentence does not silently become a front-end protocol change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean, pending, and uncovered are different states
&lt;/h2&gt;

&lt;p&gt;A diagnostic panel often has an implicit and dangerous assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;no errors shown = no errors exist&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption is not valid for an asynchronous language server. SolonCode distinguishes three cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;UI implication&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;The file was checked and no errors were found&lt;/td&gt;
&lt;td&gt;show &lt;code&gt;LSP ✓&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pending&lt;/td&gt;
&lt;td&gt;A check was requested, but no conclusion arrived within the wait budget&lt;/td&gt;
&lt;td&gt;show neither success nor error badge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;No language server covers the file, or LSP is disabled&lt;/td&gt;
&lt;td&gt;do not claim the file was checked&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;WebStreamBuilder&lt;/code&gt; builds this state without starting a process. It checks the recent file-check state first; when there is no record, it asks the manager whether any client covers the path. A covered file with no conclusion becomes &lt;code&gt;PENDING&lt;/code&gt;, not &lt;code&gt;CLEAN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The front end applies the same rule in &lt;code&gt;applyLspBadge&lt;/code&gt;: errors get an &lt;code&gt;LSP N&lt;/code&gt; badge and can expand the tool card; a confirmed clean result gets &lt;code&gt;LSP ✓&lt;/code&gt;; a pending result gets no badge. That restraint matters. A blank badge is less satisfying than a green check, but it is more honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real synchronization bug: jdtls received the change twice
&lt;/h2&gt;

&lt;p&gt;The most instructive bug appeared only after Java diagnostics became observable. The disk file was correct, but jdtls reported a duplicate method, a stray closing brace, and line numbers beyond the file’s actual length.&lt;/p&gt;

&lt;p&gt;The cause was a double application of one change. For an already-open document, the client sent both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;workspace/didChangeWatchedFiles(Changed)&lt;/code&gt;, which caused jdtls to reread the file from disk; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;textDocument/didChange&lt;/code&gt;, whose replacement range had been calculated against the old text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the first event, the server already held the new text. Applying a range sized for the old text then left part of the new document behind. The server’s document became torn even though the disk file remained clean.&lt;/p&gt;

&lt;p&gt;The fix had three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;once a document is open, use &lt;code&gt;textDocument/didChange&lt;/code&gt; rather than also sending a watched-file event; watched-file notifications remain for files that are not open;&lt;/li&gt;
&lt;li&gt;calculate a robust replacement end position using the later of the old and new text lengths;&lt;/li&gt;
&lt;li&gt;lock synchronization by URI so reading, comparing, calculating, sending, and updating the tracked version are atomic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson generalizes beyond LSP: an integration can be logically correct at the file-system boundary and still be wrong at the protocol-state boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Java adds a process-level constraint
&lt;/h2&gt;

&lt;p&gt;SolonCode itself can run on older JDKs, while jdtls requires JDK 21 or newer. A child process normally inherits the parent process’s &lt;code&gt;JAVA_HOME&lt;/code&gt;, so launching jdtls from a JDK 8 process can make it exit immediately.&lt;/p&gt;

&lt;p&gt;The implementation addresses this without rewriting the user’s settings. &lt;code&gt;JdkHomeUtil&lt;/code&gt; scans installed JDK directories and their &lt;code&gt;release&lt;/code&gt; files, chooses a suitable JDK, and &lt;code&gt;WorkspaceManager&lt;/code&gt; adds &lt;code&gt;JAVA_HOME&lt;/code&gt; to the runtime copy of the built-in Java server configuration. An explicit user setting is respected, and the persisted configuration is left untouched.&lt;/p&gt;

&lt;p&gt;That is a good example of keeping environment repair at the process boundary. The product can adapt the child process without committing a machine-specific absolute path into a project file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this architecture buys an agent user
&lt;/h2&gt;

&lt;p&gt;The result is more than “SolonCode supports LSP.” It gives each operation a useful responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;after a mutation, the agent receives actionable errors automatically;&lt;/li&gt;
&lt;li&gt;before a mutation, reads can warm the server without making file browsing feel slow;&lt;/li&gt;
&lt;li&gt;when the agent needs semantic exploration, navigation remains available explicitly;&lt;/li&gt;
&lt;li&gt;in the Web UI, diagnostics are structured, bounded, and visible;&lt;/li&gt;
&lt;li&gt;when the server is cold, unavailable, or unsupported, the UI avoids making a stronger claim than the evidence allows;&lt;/li&gt;
&lt;li&gt;when the language-server integration fails, the successful write is not turned into a failed write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last property is especially important. The repository’s hook design treats language-server failures as auxiliary failures: diagnostic and warmup hooks swallow &lt;code&gt;Throwable&lt;/code&gt; so an unavailable server cannot invalidate an otherwise successful file operation.&lt;/p&gt;

&lt;p&gt;A coding agent should not merely produce code. It should receive the feedback needed to evaluate its own changes, while exposing uncertainty to the human who reviews the result.&lt;/p&gt;

&lt;p&gt;SolonCode’s LSP work is therefore best understood as a feedback-loop design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;change -&amp;gt; synchronize -&amp;gt; wait (within a budget) -&amp;gt; filter -&amp;gt;
model repair + structured human-facing status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hard part is not connecting to a server. The hard part is preserving truth across asynchronous state, noisy diagnostics, process-level runtime differences, and two different audiences. That is what turns LSP from a rarely used tool into part of the agent’s normal engineering loop.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>automation</category>
      <category>coding</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Rewind or Fork? Two Ways to Recover a SolonCode Conversation</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Tue, 01 Sep 2026 18:41:39 +0000</pubDate>
      <link>https://dev.to/solonjava/rewind-or-fork-two-ways-to-recover-a-soloncode-conversation-4eim</link>
      <guid>https://dev.to/solonjava/rewind-or-fork-two-ways-to-recover-a-soloncode-conversation-4eim</guid>
      <description>&lt;p&gt;A coding-agent conversation rarely fails all at once. More often, it drifts.&lt;/p&gt;

&lt;p&gt;One incorrect assumption enters the context. The next answer builds on it. A correction adds more text but does not remove the original premise. Before long, the agent is consistently answering the wrong question.&lt;/p&gt;

&lt;p&gt;At other times, nothing is wrong. You simply have two plausible approaches—a minimal patch and a structural refactor—and want to explore both without sacrificing the useful conversation that led you there.&lt;/p&gt;

&lt;p&gt;Those situations look similar in a chat window, but they require opposite operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rewind&lt;/strong&gt; removes recent conversation history so you can redirect one timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork&lt;/strong&gt; preserves the original conversation and creates another timeline for a different experiment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SolonCode supports both patterns. The important part is knowing which state you want to keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shortest decision rule
&lt;/h2&gt;

&lt;p&gt;Ask one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is the recent context wrong, or is it valuable enough to preserve?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If it is wrong, rewind it. If it is valuable but you want another path, fork it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Best action&lt;/th&gt;
&lt;th&gt;What happens to the current route?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recent messages pushed the agent in the wrong direction&lt;/td&gt;
&lt;td&gt;Rewind&lt;/td&gt;
&lt;td&gt;Recent conversation history is removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The current route is valid, but you want to test an alternative&lt;/td&gt;
&lt;td&gt;Fork&lt;/td&gt;
&lt;td&gt;The original is preserved and a copy can diverge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The latest answer is wrong, but the request is still correct&lt;/td&gt;
&lt;td&gt;Re-run&lt;/td&gt;
&lt;td&gt;The last request is executed again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The current answer or task stopped before completion&lt;/td&gt;
&lt;td&gt;Continue&lt;/td&gt;
&lt;td&gt;The current work is extended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not merely UI vocabulary. Each action expresses a different intention about history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewind: edit the conversation's recent past
&lt;/h2&gt;

&lt;p&gt;SolonCode provides the built-in command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/rewind [n]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It removes the most recent &lt;code&gt;n&lt;/code&gt; messages from the current conversation context. The default is &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/rewind 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;means: discard the three most recent messages from this conversation, then continue from the earlier context that remains.&lt;/p&gt;

&lt;p&gt;The unit matters: it is &lt;strong&gt;messages&lt;/strong&gt;, not an assumed number of user/assistant pairs.&lt;/p&gt;

&lt;h3&gt;
  
  
  When rewind is useful
&lt;/h3&gt;

&lt;p&gt;Imagine that you ask SolonCode to simplify an authentication module. During the conversation, you accept an assumption that the service is stateless. Two messages later, you discover that it must preserve server-side sessions for an existing client.&lt;/p&gt;

&lt;p&gt;Adding another correction may not be enough. The old assumption remains in the conversation and can keep influencing later decisions.&lt;/p&gt;

&lt;p&gt;A rewind lets you remove that contaminated tail and replace it with a clearer direction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/rewind 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep the existing server-side session contract. Limit the change to token parsing and add regression tests for the legacy client.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are not asking the model to argue with its own recent history. You are removing that history from the active conversation context.&lt;/p&gt;

&lt;h3&gt;
  
  
  What rewind does not promise
&lt;/h3&gt;

&lt;p&gt;Rewinding a conversation is not the same as rolling back a repository.&lt;/p&gt;

&lt;p&gt;The command concerns conversation messages. Do not treat it as a replacement for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;git restore&lt;/code&gt;, &lt;code&gt;git revert&lt;/code&gt;, or resetting a branch;&lt;/li&gt;
&lt;li&gt;undoing a database migration;&lt;/li&gt;
&lt;li&gt;reversing a command already executed in a terminal;&lt;/li&gt;
&lt;li&gt;canceling a request already sent to an external service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an earlier agent run changed files, inspect the working tree and use normal version-control practices to decide what should be kept or reverted.&lt;/p&gt;

&lt;p&gt;That distinction is essential for coding agents: &lt;strong&gt;conversation state and workspace state are related, but they are not the same transaction&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fork: preserve the route and create another one
&lt;/h2&gt;

&lt;p&gt;SolonCode Web added conversation fork/copy support in v2026.7.9.&lt;/p&gt;

&lt;p&gt;Forking is appropriate when the existing conversation is useful. Instead of deleting its recent history, you copy the conversation and let the copy develop in another direction.&lt;/p&gt;

&lt;p&gt;Consider a dependency upgrade that can be handled in two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route A:&lt;/strong&gt; make the smallest compatible change;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route B:&lt;/strong&gt; refactor the integration around the new API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The analysis collected so far—affected modules, tests, compatibility requirements, and constraints—is useful to both routes. Repeating that discovery from an empty chat would waste time and might produce a different baseline.&lt;/p&gt;

&lt;p&gt;A fork gives both experiments a common conversational starting point. You can keep the original route intact while directing the copy toward the alternative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat a fork as a conversation experiment
&lt;/h3&gt;

&lt;p&gt;A useful fork has a hypothesis and the same acceptance criteria on both sides.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original conversation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implement the smallest compatible patch. Do not change public interfaces.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Forked conversation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explore a structural refactor, but preserve behavior. Report every public API change before editing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then compare the outcomes using engineering evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;changed files and diff size;&lt;/li&gt;
&lt;li&gt;test results;&lt;/li&gt;
&lt;li&gt;public API impact;&lt;/li&gt;
&lt;li&gt;migration risk;&lt;/li&gt;
&lt;li&gt;maintainability;&lt;/li&gt;
&lt;li&gt;unresolved assumptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fork is valuable because the initial context is shared. The decision should still be based on the resulting code and tests, not on which conversation sounds more confident.&lt;/p&gt;

&lt;h3&gt;
  
  
  What fork does not imply
&lt;/h3&gt;

&lt;p&gt;A copied conversation should not be confused with an isolated development environment.&lt;/p&gt;

&lt;p&gt;The available product documentation supports the conversation-copy behavior. It does not, by itself, establish that each fork receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a separate Git worktree;&lt;/li&gt;
&lt;li&gt;an isolated filesystem;&lt;/li&gt;
&lt;li&gt;a separate container;&lt;/li&gt;
&lt;li&gt;independent long-term memory;&lt;/li&gt;
&lt;li&gt;automatic branch merging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If two conversations operate on the same project directory, normal workspace coordination still matters. For genuinely parallel code experiments, create explicit Git branches or worktrees and assign each conversation a clear workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewind versus fork: rewrite or preserve
&lt;/h2&gt;

&lt;p&gt;The difference can be expressed as a timeline.&lt;/p&gt;

&lt;p&gt;Before either operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A → B → C → D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;C&lt;/code&gt; and &lt;code&gt;D&lt;/code&gt; are based on a bad assumption, rewind removes the unwanted tail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A → B → C → D
        ↓ rewind
A → B → E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is still one timeline. You changed where it goes.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;A → B → C → D&lt;/code&gt; is useful but you want to evaluate another option, fork preserves it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A → B → C → D
        └→ E → F
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now there are two conversational routes. One is not automatically better; they are two experiments from a related baseline.&lt;/p&gt;

&lt;p&gt;This leads to a reliable mental model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rewind is subtraction.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fork is preservation plus divergence.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Re-run and Continue fit
&lt;/h2&gt;

&lt;p&gt;Rewind and fork become easier to choose when placed beside two other SolonCode actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Re-run: keep the request, replace its result
&lt;/h3&gt;

&lt;p&gt;Use Re-run when the latest user request is still correct but its answer is not. You want the agent to execute that request again rather than preserve the old result.&lt;/p&gt;

&lt;p&gt;Typical intent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Same request, another attempt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Continue: keep the route, add more work
&lt;/h3&gt;

&lt;p&gt;Use Continue when the current direction is correct but incomplete. The agent may have stopped halfway through a refactor, omitted tests, or ended before reporting verification results.&lt;/p&gt;

&lt;p&gt;Typical intent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Same route, keep going.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Rewind: remove misleading recent context
&lt;/h3&gt;

&lt;p&gt;Use Rewind when the problem spans more than one answer or correction. The recent conversation itself has become a liability.&lt;/p&gt;

&lt;p&gt;Typical intent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Forget the recent messages; I want to redirect from an earlier context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Fork: keep one route and test another
&lt;/h3&gt;

&lt;p&gt;Use Fork when you do not want to destroy the current route. You need an alternative exploration with a comparable starting context.&lt;/p&gt;

&lt;p&gt;Typical intent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Preserve this route and let me try another one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A practical recovery workflow
&lt;/h2&gt;

&lt;p&gt;Suppose SolonCode is modernizing an order-processing module.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Interrupt before repairing history
&lt;/h3&gt;

&lt;p&gt;If the agent is currently running and you can already see it moving in the wrong direction, stop or steer the active run first. Rewind is for conversation history that already exists; it is not the same operation as interrupting a live execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inspect both states
&lt;/h3&gt;

&lt;p&gt;Before choosing an action, inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the last several conversation messages;&lt;/li&gt;
&lt;li&gt;the Git diff or working-tree status;&lt;/li&gt;
&lt;li&gt;commands and tests already run;&lt;/li&gt;
&lt;li&gt;external side effects, if any.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This prevents the common mistake of repairing the chat while forgetting that the workspace has already changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Choose the smallest state operation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One bad answer, same request: &lt;strong&gt;Re-run&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Correct route, unfinished work: &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Several misleading messages: &lt;strong&gt;Rewind&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Two valid approaches worth comparing: &lt;strong&gt;Fork&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Restate constraints after the operation
&lt;/h3&gt;

&lt;p&gt;Do not continue with a vague “try again.” State the corrected constraint or the fork's hypothesis explicitly.&lt;/p&gt;

&lt;p&gt;Good examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Preserve all public interfaces. Do not introduce a new cache. Run the existing integration tests before proposing additional changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;In this alternative route, optimize for maintainability rather than minimum diff size. List migration risks separately.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Verify in the repository
&lt;/h3&gt;

&lt;p&gt;Conversation controls help manage reasoning context; tests and version control validate the implementation.&lt;/p&gt;

&lt;p&gt;At minimum, review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--stat&lt;/span&gt;
git diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the project's documented build and test commands. If you used a fork for A/B exploration, apply the same acceptance criteria to both routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact checklist
&lt;/h2&gt;

&lt;p&gt;Before using Rewind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are recent messages actually harmful to later reasoning?&lt;/li&gt;
&lt;li&gt;How many messages—not turns—should be removed?&lt;/li&gt;
&lt;li&gt;Did earlier runs already modify files or external systems?&lt;/li&gt;
&lt;li&gt;Have you captured any useful findings that would otherwise be lost?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before using Fork:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the current conversation valuable enough to preserve?&lt;/li&gt;
&lt;li&gt;What exact hypothesis will the copy test?&lt;/li&gt;
&lt;li&gt;Will both routes use the same acceptance criteria?&lt;/li&gt;
&lt;li&gt;Do code experiments need separate Git branches or worktrees?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bigger lesson
&lt;/h2&gt;

&lt;p&gt;Coding-agent productivity is not only about writing a better next prompt. It is also about controlling which history the next prompt inherits.&lt;/p&gt;

&lt;p&gt;Use Rewind when recent history is part of the problem. Use Fork when recent history is an asset worth preserving. Use Re-run for another attempt at the same request, and Continue for more work on the same route.&lt;/p&gt;

&lt;p&gt;Once those intentions are separated, conversation recovery stops being trial and error. It becomes a small, explicit state-management decision—and the repository remains the final source of truth.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>java</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Treat SKILL.md Like Code: Reviewable Workflows in SolonCode</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 31 Aug 2026 23:08:52 +0000</pubDate>
      <link>https://dev.to/solonjava/treat-skillmd-like-code-reviewable-workflows-in-soloncode-42mk</link>
      <guid>https://dev.to/solonjava/treat-skillmd-like-code-reviewable-workflows-in-soloncode-42mk</guid>
      <description>&lt;p&gt;A prompt can be useful once. A Skill is different: it is an instruction package that will be discovered, selected, and reused by an agent later.&lt;/p&gt;

&lt;p&gt;That makes a Skill closer to a small software artifact than to a long prompt. It has an activation contract, an execution path, supporting resources, and failure modes. If it is vague, it can be selected at the wrong time. If it is too large, it consumes context before the actual work begins. If it changes without review, the agent's behavior changes without an obvious code diff.&lt;/p&gt;

&lt;p&gt;SolonCode's own &lt;code&gt;skill-creator&lt;/code&gt; package provides a useful structure for treating Skills as reviewable assets. This article turns that structure into a practical workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the contract, not the prose
&lt;/h2&gt;

&lt;p&gt;The smallest Skill package has a required &lt;code&gt;SKILL.md&lt;/code&gt; file. Its frontmatter must define at least:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repository-release-notes&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;skill&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;should&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;preparing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;release&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;notes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;repository's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;merged&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;changelog&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;entries."&lt;/span&gt;
&lt;span class="na"&gt;license&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Complete terms in LICENSE.txt&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the name alone. The description is the discovery contract. It tells the agent when this package is relevant.&lt;/p&gt;

&lt;p&gt;A weak description says:&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="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Helps with release notes.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A stronger description names the work and the evidence it expects:&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="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This skill should be used when converting merged pull requests, changelog entries, and release labels into a concise repository release-notes draft.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version is still short, but it gives the selector more useful signals. It also avoids claiming that the Skill handles every kind of writing.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;skill-creator&lt;/code&gt; guidance explicitly recommends specific descriptions and a third-person form such as “This skill should be used when...”. That is a small metadata rule with a large operational effect: if the package is not selected reliably, the quality of the instructions inside it does not matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the top-level instructions small
&lt;/h2&gt;

&lt;p&gt;A Skill package can contain three kinds of supporting resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repository-release-notes/
├── SKILL.md
├── scripts/
│   └── collect_changes.py
├── references/
│   └── release-style.md
└── assets/
    └── release-template.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each directory has a different job.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scripts/&lt;/code&gt; is for executable work that should be deterministic or is repeatedly rewritten.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;references/&lt;/code&gt; is for detailed material that should be loaded when the task needs it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assets/&lt;/code&gt; is for files used in the output, such as templates, icons, or starter files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more than tidy packaging. It is a context-management strategy.&lt;/p&gt;

&lt;p&gt;Put the essential procedure in &lt;code&gt;SKILL.md&lt;/code&gt;: how to recognize the task, which steps must happen, and which supporting file to open. Put long schemas, examples, and policies in &lt;code&gt;references/&lt;/code&gt;. Put reusable output material in &lt;code&gt;assets/&lt;/code&gt;. The result follows progressive disclosure: metadata is always available, the Skill body is loaded when selected, and deeper resources are loaded only when needed.&lt;/p&gt;

&lt;p&gt;A compact &lt;code&gt;SKILL.md&lt;/code&gt; might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;repository-release-notes&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This skill should be used when converting merged pull requests, changelog entries, and release labels into a concise repository release-notes draft.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Repository Release Notes&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Inspect the repository's changelog and merged changes.
&lt;span class="p"&gt;2.&lt;/span&gt; Load &lt;span class="sb"&gt;`references/release-style.md`&lt;/span&gt; before drafting.
&lt;span class="p"&gt;3.&lt;/span&gt; Use &lt;span class="sb"&gt;`scripts/collect_changes.py`&lt;/span&gt; when a deterministic change list is needed.
&lt;span class="p"&gt;4.&lt;/span&gt; Preserve links to source changes and separate verified facts from suggestions.
&lt;span class="p"&gt;5.&lt;/span&gt; Render the result with &lt;span class="sb"&gt;`assets/release-template.md`&lt;/span&gt;.
&lt;span class="p"&gt;6.&lt;/span&gt; Check the final draft against the repository's release style before returning it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is not trying to contain the whole domain. It is routing the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make examples executable or at least checkable
&lt;/h2&gt;

&lt;p&gt;A Skill is easier to review when its instructions imply a fixture that can be checked.&lt;/p&gt;

&lt;p&gt;For a release-notes Skill, a fixture could contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fixtures/release-case/
├── CHANGELOG.md
├── merged-changes.json
└── expected-draft.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fixture does not need to be part of the distributed Skill package. It can live in the repository that owns the Skill. The purpose is to answer concrete questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the description cause the Skill to be selected for the intended request?&lt;/li&gt;
&lt;li&gt;Does the procedure tell the agent where to find evidence?&lt;/li&gt;
&lt;li&gt;Does the output preserve links and uncertainty?&lt;/li&gt;
&lt;li&gt;Does the template produce a usable result rather than a generic essay?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same mindset used for code tests: make the expected behavior visible, then make regressions cheap to detect.&lt;/p&gt;

&lt;p&gt;There is one important distinction. The documented &lt;code&gt;skill-creator&lt;/code&gt; package describes the package shape and packaging validation; it does not claim that SolonCode automatically executes semantic tests for every Skill. Semantic checks remain the responsibility of the Skill author and the project that consumes it.&lt;/p&gt;

&lt;p&gt;That boundary is healthy. Syntax and structure can be validated mechanically. Whether a workflow gives good answers requires task-specific fixtures and human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the documented creation path
&lt;/h2&gt;

&lt;p&gt;When creating a new Skill from scratch, the source documents this initializer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scripts/init_skill.py repository-release-notes &lt;span class="nt"&gt;--path&lt;/span&gt; ./skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initializer creates the Skill directory, a &lt;code&gt;SKILL.md&lt;/code&gt; template, and example resource directories. After that, remove unused examples and replace the placeholders with the smallest useful package.&lt;/p&gt;

&lt;p&gt;The workflow is deliberately staged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;understand examples
        ↓
plan scripts, references, and assets
        ↓
initialize the package
        ↓
write the contract and procedure
        ↓
run a real fixture
        ↓
package and review the result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents a common failure mode: writing a large instruction file first and only later discovering that half of it belongs in a reference document or a deterministic script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Package validation is a release gate
&lt;/h2&gt;

&lt;p&gt;After editing, the documented packaging command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scripts/package_skill.py ./skills/repository-release-notes ./dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package step validates the YAML frontmatter, naming conventions, directory structure, description quality, and resource references before creating a zip archive.&lt;/p&gt;

&lt;p&gt;That validation is useful, but it is not a substitute for behavioral review. A package can have valid frontmatter and still have an ambiguous trigger. It can have a correct directory structure and still instruct the agent to invent facts. Treat the package command as a release gate for structure, then run the fixture as a release gate for behavior.&lt;/p&gt;

&lt;p&gt;A practical review checklist looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] name is stable and descriptive
[ ] description says when the Skill should be used
[ ] SKILL.md contains the short procedure, not the whole encyclopedia
[ ] every referenced file exists
[ ] scripts have a deterministic reason to exist
[ ] references are loaded only when needed
[ ] assets are output resources, not hidden instructions
[ ] examples cover the important path and at least one failure path
[ ] package validation passes
[ ] a reviewer can explain how to roll back the change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last item is easy to overlook. A Skill changes agent behavior. It should be possible to revert it by reverting a directory change or selecting the previous package, just as a code change can be rolled back to a known commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Skills in the SolonCode Web workflow
&lt;/h2&gt;

&lt;p&gt;SolonCode exposes Skills as a managed part of the workspace rather than treating them as invisible prompt text. The Web settings code renders Skill names, descriptions, and paths for mounted sources, and provides management actions.&lt;/p&gt;

&lt;p&gt;That makes a useful authoring loop possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;edit the Skill in a workspace;&lt;/li&gt;
&lt;li&gt;inspect its metadata and source location in the Web settings surface;&lt;/li&gt;
&lt;li&gt;run a concrete task against a fixture;&lt;/li&gt;
&lt;li&gt;revise the package;&lt;/li&gt;
&lt;li&gt;refresh the runtime's Skill discovery when needed;&lt;/li&gt;
&lt;li&gt;rerun the same fixture;&lt;/li&gt;
&lt;li&gt;keep or revert the package based on the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The runtime reference used by SolonCode documents &lt;code&gt;engine.refreshSkills()&lt;/code&gt; as the refresh operation. The exact refresh timing should still be verified in the running product and version being used; a source-level method name is not a promise that every client refreshes at the same moment.&lt;/p&gt;

&lt;p&gt;This is where a Skill starts to feel like code review. The author can show the package, the fixture, and the observed result instead of saying only “the prompt seems to work.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Use task queueing to run the review loop without losing your train of thought
&lt;/h2&gt;

&lt;p&gt;Skill authoring often produces a sequence of related checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;inspect the package;&lt;/li&gt;
&lt;li&gt;run the fixture;&lt;/li&gt;
&lt;li&gt;compare the output with the expected draft;&lt;/li&gt;
&lt;li&gt;inspect the failure case;&lt;/li&gt;
&lt;li&gt;update the description;&lt;/li&gt;
&lt;li&gt;package the result again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SolonCode's Web interface supports queued follow-up messages for this kind of loop. While a task is running, a follow-up can wait in the queue and run after the current task. The queue is persisted through &lt;code&gt;/web/chat/queue&lt;/code&gt; and the session's &lt;code&gt;queue-tasks.json&lt;/code&gt;; after a refresh, queued text is restored for review but is not automatically sent.&lt;/p&gt;

&lt;p&gt;That last behavior matters. Restoring a queue should not unexpectedly start a chain of model calls. Cold restore hydrates the UI; the user explicitly resumes it.&lt;/p&gt;

&lt;p&gt;Steering is a different tool. An Enter submission during a running task is sent to &lt;code&gt;/web/chat/steer&lt;/code&gt;. The message first appears as pending, then becomes visible when the backend reports &lt;code&gt;system.steer_applied&lt;/code&gt;. If the run changes or the steering mailbox is full, the message is demoted to the normal queue rather than silently discarded.&lt;/p&gt;

&lt;p&gt;The distinction is useful when reviewing a Skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Steer = change the current investigation
Queue = run the next check after the current investigation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, if the agent is testing the wrong fixture, steer it: “Use the failure-case fixture instead.” If the current test is correct and you simply want the next comparison, queue: “Now compare the output with expected-draft.md.”&lt;/p&gt;

&lt;p&gt;The implementation also gives steering a deliberate boundary. &lt;code&gt;SteerInterceptor&lt;/code&gt; stores pending text in a transient session mailbox, injects it at a reasoning boundary, and does not interrupt an in-flight model stream or tool call. It guards the first turn and avoids injecting while the last assistant message still has open tool calls. If the task ends before the message is consumed, the backend emits a dropped event and the frontend moves it to the regular queue.&lt;/p&gt;

&lt;p&gt;This is a good model for safe workflow control: immediate feedback does not have to mean unsafe interruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small operating procedure for Skill changes
&lt;/h2&gt;

&lt;p&gt;Here is a repeatable review sequence for a real Skill repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Read the current package
&lt;/h3&gt;

&lt;p&gt;Record the &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, instruction steps, and every referenced resource. Do not review only the prose; the trigger metadata is part of the behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Identify one intended request and one near miss
&lt;/h3&gt;

&lt;p&gt;Write a request that should select the Skill and another that should not. This tests whether the description is specific or merely enthusiastic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Run the happy-path fixture
&lt;/h3&gt;

&lt;p&gt;Use a small, known input and record the expected output shape. Preserve source links and mark any inference as an inference.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Queue the next check
&lt;/h3&gt;

&lt;p&gt;While the first check is running, queue the failure-case comparison. Keep the current task focused rather than stuffing every future instruction into the active prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Steer only when the current direction is wrong
&lt;/h3&gt;

&lt;p&gt;If the agent starts inspecting the wrong file or uses the wrong fixture, steer it. Do not use steering as a replacement for a clearly ordered test plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Package-validate the change
&lt;/h3&gt;

&lt;p&gt;Run the documented packaging command. Fix missing references, invalid metadata, or structural problems before asking anyone else to consume the package.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Review the diff and keep a rollback point
&lt;/h3&gt;

&lt;p&gt;A Skill is behavior. Review it like behavior: what activates it, what it can cause the agent to do, and how to return to the previous version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The larger lesson
&lt;/h2&gt;

&lt;p&gt;The most useful Skills are not the longest ones. They are the ones with a precise activation contract, a short procedure, well-separated supporting material, and a checkable result.&lt;/p&gt;

&lt;p&gt;SolonCode's package structure makes those boundaries visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SKILL.md&lt;/code&gt; explains what to do and when;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/&lt;/code&gt; makes repeatable work deterministic;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;references/&lt;/code&gt; keeps detailed knowledge available without bloating every run;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assets/&lt;/code&gt; supplies reusable output material;&lt;/li&gt;
&lt;li&gt;package validation catches structural mistakes;&lt;/li&gt;
&lt;li&gt;fixtures and review catch behavioral mistakes;&lt;/li&gt;
&lt;li&gt;steering and queueing let the author control an active review without losing the next planned check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once Skills are treated as code-like assets, “prompt tweaking” becomes an engineering loop: define, test, inspect, package, observe, and roll back when necessary.&lt;/p&gt;

&lt;p&gt;Source reviewed: &lt;a href="https://github.com/opensolon/soloncode" rel="noopener noreferrer"&gt;opensolon/soloncode&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Key source paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/release/skills/skill-creator/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/SteerInterceptor.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/resources/static/js/app-streaming.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/resources/static/js/app-settings-mounts.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/portal/web/WebController.java&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>productivity</category>
    </item>
    <item>
      <title>One Agent Core, Six Entry Points: How SolonCode Reuses Its Runtime</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:53:30 +0000</pubDate>
      <link>https://dev.to/solonjava/one-agent-core-six-entry-points-how-soloncode-reuses-its-runtime-41l4</link>
      <guid>https://dev.to/solonjava/one-agent-core-six-entry-points-how-soloncode-reuses-its-runtime-41l4</guid>
      <description>&lt;p&gt;A coding agent rarely stays inside one interface.&lt;/p&gt;

&lt;p&gt;A developer may want an interactive terminal for exploration, a browser for reviewing tool calls and Git changes, a desktop workspace for editing, a one-shot command for CI, a persistent process for an SDK, or an IDE connection through the Agent Client Protocol (ACP).&lt;/p&gt;

&lt;p&gt;The easy implementation is to build a separate agent for each surface. The result is usually six versions of model selection, permissions, sessions, tools, and error handling that slowly drift apart.&lt;/p&gt;

&lt;p&gt;SolonCode takes a different route. Its public Java source exposes six user-facing surfaces—&lt;strong&gt;interactive CLI, Web, Desktop, one-shot &lt;code&gt;run&lt;/code&gt;, persistent &lt;code&gt;stream&lt;/code&gt;, and ACP&lt;/strong&gt;—backed by five explicit CLI modes plus the interactive fallback, all built around the same Agent composition.&lt;/p&gt;

&lt;p&gt;That does not mean the six interfaces are identical. It means their product-specific adapters sit around a shared core instead of reimplementing the coding agent each time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr973lx6ync7xumve7c3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr973lx6ync7xumve7c3t.png" alt="SolonCode interactive CLI running in a project workspace" width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;At a high level, the design looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Interactive CLI ─┐
Web UI ──────────┤
Desktop ─────────┤
run ─────────────┼──&amp;gt; WorkspaceContext ──&amp;gt; HarnessEngine
stream ──────────┤          │                    │
ACP ─────────────┘          │                    ├─ models
                            │                    ├─ tools
                            ├─ sessions           ├─ skills / agents
                            ├─ loop scheduler     ├─ permissions
                            ├─ file watcher       └─ memory
                            └─ workspace settings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important class is not a UI component. It is the workspace context assembled by the Java backend.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;Configurator.agentRuntime(...)&lt;/code&gt;, SolonCode initializes the default workspace, asks &lt;code&gt;WorkspaceManager&lt;/code&gt; for that context, captures the context's Loop scheduler, and returns the context's &lt;code&gt;HarnessEngine&lt;/code&gt;. The command dispatcher then connects the selected entry path to that runtime.&lt;/p&gt;

&lt;p&gt;There is an important precision here: this is a &lt;strong&gt;shared composition model&lt;/strong&gt;, not a promise that every workspace in every process uses one JVM-global engine. SolonCode Web supports workspace-specific contexts. The architectural point is that those contexts are built through the same runtime path rather than each interface inventing its own agent stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Interactive CLI: the direct conversation adapter
&lt;/h2&gt;

&lt;p&gt;With no special mode selected, SolonCode starts &lt;code&gt;CliShell&lt;/code&gt; on an interactive thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most direct product surface: prompt, streaming response, tool activity, commands, Skills, and session continuity in a terminal. It is useful when the terminal is already the developer's main control plane.&lt;/p&gt;

&lt;p&gt;The CLI adapter owns terminal behavior—welcome text, keyboard interaction, command completion, and rendering—but it does not need its own model registry or separate tool implementation. Those remain runtime concerns.&lt;/p&gt;

&lt;p&gt;That separation matters for maintenance. Adding a tool to the runtime should not require rebuilding the tool six times just because users can reach it from six places.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Web: a richer view over workspace state
&lt;/h2&gt;

&lt;p&gt;The Web entry path is selected with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode web 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing &lt;code&gt;0&lt;/code&gt; asks SolonCode to probe for an available ephemeral port. Unlike the default CLI path, Web mode explicitly enables HTTP and WebSocket support. It registers the Web gate and the controllers for chat, settings, Skills, models, MCP, OpenAPI, LSP, memory, authentication, and remote runs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6q2t67tyh2h90nfwf64j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6q2t67tyh2h90nfwf64j.png" alt="SolonCode Web with the conversation workspace, file tree, and development controls" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a good example of an adapter adding product value without replacing the core. The browser can provide things a terminal cannot express as comfortably:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file-tree navigation;&lt;/li&gt;
&lt;li&gt;structured tool cards;&lt;/li&gt;
&lt;li&gt;model and provider settings;&lt;/li&gt;
&lt;li&gt;Skills management;&lt;/li&gt;
&lt;li&gt;session controls;&lt;/li&gt;
&lt;li&gt;Git status and diff review;&lt;/li&gt;
&lt;li&gt;Loop task configuration;&lt;/li&gt;
&lt;li&gt;real-time updates over WebSocket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser is not “the agent.” It is a client and control surface around workspace-aware Agent execution.&lt;/p&gt;

&lt;p&gt;The source also avoids turning HTTP into an accidental dependency of every mode. &lt;code&gt;App&lt;/code&gt; disables HTTP by default and enables it only for modes that need it, such as &lt;code&gt;web&lt;/code&gt; and &lt;code&gt;serve&lt;/code&gt;. ACP explicitly keeps HTTP and WebSocket disabled because its transport is standard input/output.&lt;/p&gt;

&lt;p&gt;That is a small but valuable boundary: a headless run should not quietly start a server just because the same application can also serve a browser UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Desktop: an IDE surface, not a second Agent backend
&lt;/h2&gt;

&lt;p&gt;SolonCode Desktop combines a Tauri/React interface with the Java CLI backend.&lt;/p&gt;

&lt;p&gt;For development, the documented flow starts the backend separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode serve 4808
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Desktop client then connects over HTTP and WebSocket. Its UI adds Monaco editing, an integrated terminal, file operations, Git controls, local workspace management, and native application behavior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fts5c7j5s5w7j44mmzt13.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fts5c7j5s5w7j44mmzt13.png" alt="SolonCode Desktop combining Agent chat, project files, editor, terminal, and Git" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The responsibilities are deliberately split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React / TypeScript UI
  ├─ layout and editor UX
  ├─ Agent conversation UI
  └─ HTTP + WebSocket client

Tauri / Rust native layer
  ├─ filesystem operations
  ├─ integrated terminal
  └─ CLI process management

Java CLI backend (`soloncode serve`)
  ├─ Agent runtime
  ├─ models and tools
  ├─ sessions
  ├─ Skills and memory
  └─ workspace execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Desktop therefore does not need to ship a second implementation of reasoning, tool permission rules, or model invocation. It specializes in the IDE experience.&lt;/p&gt;

&lt;p&gt;This is also why “same core” should not be misread as “same UI.” Web and Desktop can expose different controls, persistence choices, and editing affordances while still delegating Agent work to the same backend architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;run&lt;/code&gt;: one prompt, one lifecycle, a useful exit code
&lt;/h2&gt;

&lt;p&gt;Interactive products are only half the story. Automation needs a command that completes one task, can emit a machine-readable result, and exits with a meaningful status code.&lt;/p&gt;

&lt;p&gt;That is the role of &lt;code&gt;soloncode run&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"Inspect this repository and return the three highest-risk modules"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; plan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-turns&lt;/span&gt; 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PrintMode&lt;/code&gt; makes the lifecycle explicit. It resolves a prompt from the argument or standard input, applies runtime options, resolves a session, runs the Agent stream to completion, renders the chosen output, and returns an explicit exit code.&lt;/p&gt;

&lt;p&gt;The source distinguishes at least these outcomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 success
1 execution error
2 maximum turns exceeded
3 missing prompt
4 budget exceeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes &lt;code&gt;run&lt;/code&gt; suitable for shell scripts and CI jobs because success is not inferred from a sentence in a chat transcript.&lt;/p&gt;

&lt;p&gt;It also supports three output styles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;text&lt;/code&gt; for a human-readable final answer;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;json&lt;/code&gt; for one result object;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stream-json&lt;/code&gt; for JSONL output during one run; initialization and final-result events are always emitted, while intermediate assistant and tool events require &lt;code&gt;--verbose&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last item needs careful wording. Streamed output does not make &lt;code&gt;run&lt;/code&gt; a persistent multi-turn process. The implementation rejects persistent stream input in &lt;code&gt;run&lt;/code&gt; and tells callers to use the dedicated &lt;code&gt;stream&lt;/code&gt; command instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. &lt;code&gt;stream&lt;/code&gt;: a process that stays alive
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;soloncode stream&lt;/code&gt; is a different lifecycle, not an alias for &lt;code&gt;run&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode stream &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is designed for a parent process or SDK that communicates through JSONL while keeping the Agent process alive across turns. A persistent consumer can reuse context without forking a new Java process for every message.&lt;/p&gt;

&lt;p&gt;This separation avoids an ambiguous command whose behavior changes depending on a hidden combination of flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run     = one task, then exit
stream  = persistent JSONL input/output channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction is especially useful for integration code. A CI step normally wants the determinism of &lt;code&gt;run&lt;/code&gt;; an SDK bridge normally wants the process lifetime of &lt;code&gt;stream&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The shared runtime still matters. Persistent transport does not require a second tool catalog or a reduced “automation agent.” It changes how prompts and events cross the process boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. ACP: translate Agent events instead of rebuilding the Agent
&lt;/h2&gt;

&lt;p&gt;ACP is the protocol-facing entry path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode acp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AcpLink&lt;/code&gt; creates an ACP agent over standard input/output. When an ACP prompt arrives, it converts the request into a Solon AI &lt;code&gt;Prompt&lt;/code&gt;, obtains an Agent session, and runs the request through the injected runtime's prompt–session–stream pipeline.&lt;/p&gt;

&lt;p&gt;Then it translates runtime events into protocol updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plan events become ACP plan entries;&lt;/li&gt;
&lt;li&gt;reasoning deltas can become thought updates;&lt;/li&gt;
&lt;li&gt;tool starts become in-progress tool-call cards;&lt;/li&gt;
&lt;li&gt;tool completions become completed or failed updates;&lt;/li&gt;
&lt;li&gt;file edits can become structured diffs;&lt;/li&gt;
&lt;li&gt;the final run event becomes the final ACP message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is adapter code in the best sense. ACP defines how an IDE or client sees the work, while &lt;code&gt;HarnessEngine&lt;/code&gt; defines how the work is done.&lt;/p&gt;

&lt;p&gt;Not every internal event is exposed. SolonCode filters internal task-dispatch, memory, and Goal tools from ACP tool cards. That is another sign of a deliberate boundary: sharing a runtime does not mean leaking every internal mechanism into every protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually shared?
&lt;/h2&gt;

&lt;p&gt;“Shared core” can become vague architecture marketing, so it helps to name the concrete concerns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model selection and invocation
&lt;/h3&gt;

&lt;p&gt;The adapters do not each implement provider calls. They select or pass model context into the Agent runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools and permissions
&lt;/h3&gt;

&lt;p&gt;File reads, edits, search, terminal execution, Web access, and extension tools belong to the runtime. Entry paths can change permission behavior—for example, a headless plan run denies write-oriented tools—but they apply that policy to the same tool system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sessions
&lt;/h3&gt;

&lt;p&gt;Each adapter resolves sessions in a way appropriate to its lifecycle. An ACP session is not automatically the same identifier as a Web session, and a one-shot run may create its own print session. What is shared is the session abstraction and Agent execution path, not magical cross-client session synchronization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skills, agents, MCP, and memory
&lt;/h3&gt;

&lt;p&gt;These capabilities are composed into the engine and workspace. An adapter can expose management UI or choose a stripped-down mode such as &lt;code&gt;--bare&lt;/code&gt;, but it does not need to invent an incompatible extension model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workspace boundaries
&lt;/h3&gt;

&lt;p&gt;The workspace context brings together the engine and workspace services. Web can manage more than the default context, which is why the safest architecture statement is “one composition path per workspace,” not “one global Agent object for everything.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What remains adapter-specific?
&lt;/h2&gt;

&lt;p&gt;A clean shared core does not erase product differences.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry path&lt;/th&gt;
&lt;th&gt;Adapter-specific concern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interactive CLI&lt;/td&gt;
&lt;td&gt;terminal rendering, keyboard flow, slash commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;HTTP/WebSocket, browser state, settings panels, Git and task UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop&lt;/td&gt;
&lt;td&gt;native window, Monaco, terminal, local file and Git experience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one-shot lifecycle, output format, exit status, automation options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stream&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;persistent JSONL transport and turn interruption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACP&lt;/td&gt;
&lt;td&gt;protocol capabilities, session requests, structured plan/tool updates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That table is the design in practical terms: reuse the Agent, specialize the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this architecture is useful to users
&lt;/h2&gt;

&lt;p&gt;Most users will never open &lt;code&gt;Configurator.java&lt;/code&gt;, but they still feel the consequences of the design.&lt;/p&gt;

&lt;h3&gt;
  
  
  A feature can travel across surfaces
&lt;/h3&gt;

&lt;p&gt;When a capability belongs to the core, a new client does not have to start from zero. It still needs adapter work and UI, but the model, tool, Skill, permission, and workspace foundations already exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automation is not a fragile scrape of the UI
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;run&lt;/code&gt;, &lt;code&gt;stream&lt;/code&gt;, and ACP are first-class entry paths. A script does not need to drive browser buttons, and an IDE does not need to parse colored terminal output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interfaces can stay focused
&lt;/h3&gt;

&lt;p&gt;The CLI can remain fast and direct. Web can prioritize review and settings. Desktop can prioritize editing. ACP can prioritize protocol fidelity. They do not all need to become the same giant application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime behavior is easier to audit
&lt;/h3&gt;

&lt;p&gt;The public dispatch path is visible in one place. A reviewer can trace which command enables HTTP, which adapter receives the runtime, and how protocol events are mapped.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical way to choose an entry path
&lt;/h2&gt;

&lt;p&gt;Use the interface that matches the lifecycle of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;interactive CLI&lt;/strong&gt; when you are living in a terminal and want a direct conversation.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Web&lt;/strong&gt; when you want browser-based review, settings, sessions, Git visibility, and task controls.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Desktop&lt;/strong&gt; when Agent work should sit next to editing, files, terminal, and Git in one native workspace.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;&lt;code&gt;run&lt;/code&gt;&lt;/strong&gt; when a script or CI step has one bounded task and needs a result plus exit code.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;&lt;code&gt;stream&lt;/code&gt;&lt;/strong&gt; when a parent process needs a persistent JSONL conversation.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;ACP&lt;/strong&gt; when an ACP-compatible client or IDE should receive structured plans, tool calls, diffs, and results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The choice changes the interaction contract. It does not require choosing a different SolonCode Agent implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader engineering lesson
&lt;/h2&gt;

&lt;p&gt;Multi-interface Agent products have two bad extremes.&lt;/p&gt;

&lt;p&gt;The first is duplication: every client builds its own Agent stack, and behavior drifts. The second is false uniformity: every client is forced through one UI-shaped API even when a terminal, CI process, desktop editor, and protocol client have different needs.&lt;/p&gt;

&lt;p&gt;SolonCode's source shows a more useful middle path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;compose the Agent at the workspace layer;&lt;/li&gt;
&lt;li&gt;keep models, tools, permissions, sessions, Skills, and memory in that core;&lt;/li&gt;
&lt;li&gt;give each entry path a lifecycle-specific adapter;&lt;/li&gt;
&lt;li&gt;enable network servers only where they are needed;&lt;/li&gt;
&lt;li&gt;translate runtime events into the native language of each surface.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is not six identical products. It is one open Agent architecture with six honest boundaries.&lt;/p&gt;

&lt;p&gt;Source reviewed: &lt;a href="https://github.com/opensolon/soloncode" rel="noopener noreferrer"&gt;https://github.com/opensolon/soloncode&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Key implementation paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/Configurator.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/App.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/portal/printmode/PrintMode.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-cli/src/main/java/org/noear/solon/codecli/portal/acp/AcpLink.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;soloncode-desktop/README.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>java</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
