<?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: Lago Moreno</title>
    <description>The latest articles on DEV Community by Lago Moreno (@lagomor).</description>
    <link>https://dev.to/lagomor</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%2F4197618%2Fb5686d9a-41fb-4396-a9ab-7bc0fe037c8b.jpg</url>
      <title>DEV Community: Lago Moreno</title>
      <link>https://dev.to/lagomor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lagomor"/>
    <language>en</language>
    <item>
      <title>Hi everyone! I’m Moreno, a Java developer and consultant from Italy. In the evenings I dig into LLM agents and what breaks in production: broken JSON, provider outages, agents doing what they shouldn’t.</title>
      <dc:creator>Lago Moreno</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:22:06 +0000</pubDate>
      <link>https://dev.to/lagomor/hi-everyone-im-moreno-a-java-developer-and-consultant-from-italy-in-the-evenings-i-dig-into-llm-2dh5</link>
      <guid>https://dev.to/lagomor/hi-everyone-im-moreno-a-java-developer-and-consultant-from-italy-in-the-evenings-i-dig-into-llm-2dh5</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/sloan/welcome-thread-v-397-39lm" class="crayons-story__hidden-navigation-link"&gt;Welcome Thread - v 397&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/sloan" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F31047%2Faf153cd6-9994-4a68-83f4-8ddf3e13f0bf.jpg" alt="sloan profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/sloan" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Sloan the DEV Moderator
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Sloan the DEV Moderator
                
                
              
              &lt;div id="story-author-preview-content-4766435" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/sloan" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F31047%2Faf153cd6-9994-4a68-83f4-8ddf3e13f0bf.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Sloan the DEV Moderator&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/sloan/welcome-thread-v-397-39lm" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/sloan/welcome-thread-v-397-39lm" id="article-link-4766435"&gt;
          Welcome Thread - v 397
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/welcome"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;welcome&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/sloan/welcome-thread-v-397-39lm" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;51&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/sloan/welcome-thread-v-397-39lm#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              666&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            1 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>agents</category>
      <category>java</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your Java LLM agent works in the demo. Friday night in production is another story.</title>
      <dc:creator>Lago Moreno</dc:creator>
      <pubDate>Wed, 21 Oct 2026 08:35:16 +0000</pubDate>
      <link>https://dev.to/lagomor/your-java-llm-agent-works-in-the-demo-friday-night-in-production-is-another-story-4bb8</link>
      <guid>https://dev.to/lagomor/your-java-llm-agent-works-in-the-demo-friday-night-in-production-is-another-story-4bb8</guid>
      <description>&lt;p&gt;&lt;strong&gt;Friday, 6:40 PM.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Picture an agent that handles customer refunds. In the demo it was perfect: it reads the request, checks the order, calls the &lt;code&gt;refund&lt;/code&gt; tool and returns clean JSON. Everyone is happy.&lt;/p&gt;

&lt;p&gt;Then comes the first Friday night in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6:40 PM.&lt;/strong&gt; The provider starts returning 503s. Every request waits for the timeout before failing, and the queue keeps growing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6:52 PM.&lt;/strong&gt; The model decides to wrap its JSON in a Markdown code fence. The downstream service can't parse it anymore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7:15 PM.&lt;/strong&gt; A customer asks for a €4,000 refund. The agent calls &lt;code&gt;refund&lt;/code&gt;. Nobody thought to put a check in front of an action like that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monday morning, two questions arrive: what did that weekend cost, and what exactly did the agent do? Nobody can answer with confidence.&lt;/p&gt;

&lt;p&gt;The scene is made up. But if you've run an LLM agent in production, you've probably met at least one piece of it. And notice: &lt;strong&gt;none of these problems is about how smart the model is.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://github.com/xmor/ara" rel="noopener noreferrer"&gt;ARA&lt;/a&gt; is an open source (Apache 2.0) Java 21 runtime for LLM agents, built on LangChain4j. It handles output contracts, human approval, provider failover, spend caps and execution traces, and runs fully offline for tests. On Maven Central as &lt;code&gt;io.github.xmor:ara-runtime:1.0.3&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The loop is easy. The rest is the problem.
&lt;/h2&gt;

&lt;p&gt;Stripped down, an agent is a loop: send a request to the model, run the tool it picked, hand back the result, repeat until there's a final answer. You can write that in an afternoon, which is why so many tutorials stop there.&lt;/p&gt;

&lt;p&gt;The real work starts after it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you stop malformed output from reaching downstream systems?&lt;/li&gt;
&lt;li&gt;How do you stop an agent from taking a critical action without sign-off?&lt;/li&gt;
&lt;li&gt;What happens when the provider is down?&lt;/li&gt;
&lt;li&gt;How do you cap cost?&lt;/li&gt;
&lt;li&gt;How do you reconstruct a decision when something goes wrong?&lt;/li&gt;
&lt;li&gt;How do you test all of this without a live API?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These share one trait: you shouldn't leave them to the model. An LLM can help decide &lt;em&gt;what&lt;/em&gt; to do. It shouldn't be the only thing responsible for the rules that govern how that decision gets executed.&lt;/p&gt;

&lt;p&gt;I work on the JVM, and I wanted these rules in one place: explicit, in Java code you can read, with tests you can repeat. That's how ARA started.&lt;/p&gt;

&lt;h2&gt;
  
  
  ARA in thirty seconds
&lt;/h2&gt;

&lt;p&gt;ARA (&lt;em&gt;Agent Runtime Architecture&lt;/em&gt;) is an open source runtime for LLM agents. It's written in Java 21, licensed under Apache 2.0 and published on Maven Central.&lt;/p&gt;

&lt;p&gt;It's not a new model, and it doesn't replace the LLM integration layer. It sits on top of &lt;a href="https://github.com/langchain4j/langchain4j" rel="noopener noreferrer"&gt;LangChain4j&lt;/a&gt;, which handles the providers (OpenAI, Anthropic, Ollama, Mistral, any OpenAI-compatible endpoint), and takes care of how agents &lt;em&gt;run&lt;/em&gt;. It's a library, not a container: no mandatory annotations, no reflection to discover components, no Spring requirement.&lt;/p&gt;

&lt;p&gt;The principle behind it: &lt;strong&gt;the model's probabilistic decisions and the runtime's deterministic rules should have separate owners.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to follow along with the code, the repository is &lt;a href="https://github.com/xmor/ara" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. Let's go back to Friday night.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Model output is not a contract (6:52 PM)
&lt;/h2&gt;

&lt;p&gt;The model wraps its JSON in a Markdown fence, or forgets a required field. The downstream service expects structured data and breaks.&lt;/p&gt;

&lt;p&gt;A common fix is asking a second LLM to check the first one's answer. But a deterministic validation doesn't need another model call.&lt;/p&gt;

&lt;p&gt;In ARA you declare an &lt;code&gt;AgentContract&lt;/code&gt; with checks written in Java:&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;AgentContract&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentContract&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="c1"&gt;// Input side: runs before the model sees anything&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addInputProcessor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InputSanitizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;     &lt;span class="c1"&gt;// blocks known prompt-injection patterns&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addInputProcessor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContentTruncator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;// hard cap on input length&lt;/span&gt;
        &lt;span class="c1"&gt;// Declares the expected shape and appends it to the system prompt&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputSchema&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonSchemaValidator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SCHEMA&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;// Output side: plain Java, zero tokens, applied in order&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addOutputProcessor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MarkdownFenceStripper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;          &lt;span class="c1"&gt;// strips the Markdown code fence around the JSON (6:52 PM)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addOutputProcessor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonSchemaValidator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forOutput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SCHEMA&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;// rejects missing or wrong fields&lt;/span&gt;
        &lt;span class="c1"&gt;// On rejection, send the answer back with the reason, at most twice (these calls cost tokens)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputRepairAttempts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// The contract is bound to the agent: every execute() goes through it&lt;/span&gt;
&lt;span class="nc"&gt;AraAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAgent&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;contract&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two separate mechanisms are at work.&lt;/p&gt;

&lt;p&gt;The first is deterministic. &lt;code&gt;MarkdownFenceStripper&lt;/code&gt; removes the 6:52 PM code fence before the downstream service ever sees it, and the schema validator catches the missing field. These checks run in plain Java and consume no tokens.&lt;/p&gt;

&lt;p&gt;The second is recovery. With &lt;code&gt;outputRepairAttempts(2)&lt;/code&gt;, the model gets its rejected answer back along with the reason, and two chances to fix it. Those calls do cost tokens, so account for them separately.&lt;/p&gt;

&lt;p&gt;If the answer still doesn't satisfy the contract, the task fails explicitly instead of passing invalid data along.&lt;/p&gt;

&lt;p&gt;The point isn't to make the model infallible. It's to make sure its mistakes don't get accepted unchecked.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Some actions should wait for a human (7:15 PM)
&lt;/h2&gt;

&lt;p&gt;An agent can have access to the right tool, understand the request and even write a convincing justification. None of that means it should execute immediately.&lt;/p&gt;

&lt;p&gt;In ARA you configure an approval gate:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Holds pending requests in memory (they don't survive a restart, see the limits below)&lt;/span&gt;
&lt;span class="nc"&gt;ApprovalGate&lt;/span&gt; &lt;span class="n"&gt;gate&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;InMemoryApprovalGate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Level 1: enable approvals on the runtime&lt;/span&gt;
&lt;span class="nc"&gt;AraRuntime&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AraRuntime&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;llmClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llmClient&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;approvalGate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="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;AraAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;AgentConfig&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;agentId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"refund-agent"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"support"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;// Level 2: opt this agent in; every tool call now waits for a human&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;humanApprovalRequired&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;// Tools are opt-in per agent: this one can only call "refund"&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enabledTools&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"refund"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&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;From then on, when the agent calls &lt;code&gt;refund&lt;/code&gt;, the runtime parks the call and creates an approval request carrying the tool, its arguments and an expiry. Thanks to Java 21 virtual threads, waiting doesn't hold a platform thread, so many pending requests don't cost you resources.&lt;/p&gt;

&lt;p&gt;The operator has three answers: &lt;strong&gt;approve&lt;/strong&gt; (the refund goes through as is), &lt;strong&gt;reject with a reason&lt;/strong&gt; (the agent receives it and can change course), or &lt;strong&gt;modify the arguments&lt;/strong&gt; (correct the amount to €400 and the refund goes through with that).&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What the operator's UI (or your own endpoint) calls, one of these per request&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&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;ApprovalDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Approved&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;                &lt;span class="c1"&gt;// run as proposed&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&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;ApprovalDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Rejected&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Too expensive"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// the agent sees the reason&lt;/span&gt;
&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&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;ApprovalDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Modified&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newArgumentJson&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// run with corrected arguments, e.g. €400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;ApprovalDecision&lt;/code&gt; is a sealed interface, so a &lt;code&gt;switch&lt;/code&gt; over it must handle all three cases or it won't compile:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sealed interface: remove a case and this no longer compiles&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;decision&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="nc"&gt;ApprovalDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Approved&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;proceed&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;ApprovalDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Rejected&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;ApprovalDecision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Modified&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;proceed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newPayload&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;proceed()&lt;/code&gt; and &lt;code&gt;abort()&lt;/code&gt; stand in for your own code.)&lt;/p&gt;

&lt;p&gt;If nobody answers before the deadline, the call fails. The default is to do nothing.&lt;/p&gt;

&lt;p&gt;One thing to know: the gate applies per agent, not per tool. With &lt;code&gt;humanApprovalRequired(true)&lt;/code&gt;, &lt;em&gt;every&lt;/em&gt; tool call of that agent waits for a human. So keep sensitive actions in a dedicated agent, like the &lt;code&gt;refund-agent&lt;/code&gt; above, and let read-only agents run without the gate.&lt;/p&gt;

&lt;p&gt;The value isn't an "Approve" button. It's an explicit control point between the agent's decision and the action that changes the outside world. &lt;strong&gt;An agent can propose an action without having the authority to execute it.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3. When the provider goes down (6:40 PM)
&lt;/h2&gt;

&lt;p&gt;At 6:40 the provider answers 503.&lt;/p&gt;

&lt;p&gt;A retry may help, but repeating the same request against an unavailable service isn't a resilience strategy.&lt;/p&gt;

&lt;p&gt;In ARA you configure an ordered chain of models:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// "smart", "local" and "cheap" are names of clients registered on the runtime:&lt;/span&gt;
&lt;span class="c1"&gt;// no credentials live in the agent config&lt;/span&gt;
&lt;span class="nc"&gt;AgentConfig&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resilient"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primaryLlm&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LlmProfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"smart"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fallbackLlms&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;              &lt;span class="c1"&gt;// tried in declaration order&lt;/span&gt;
                &lt;span class="nc"&gt;LlmProfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"local"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                &lt;span class="nc"&gt;LlmProfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cheap"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;// Move to the next model only on retryable errors (network, 5xx, 429);&lt;/span&gt;
        &lt;span class="c1"&gt;// auth errors and bad requests stop the chain&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;llmSelectionPolicy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LlmSelectionPolicy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FAILOVER&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The interesting part is deciding &lt;em&gt;when&lt;/em&gt; to switch. A network error, a 5xx or a rate limit moves on to the next model. An invalid API key or a malformed request stops the chain instead: it would fail identically on every model, and switching would only clutter your logs.&lt;/p&gt;

&lt;p&gt;Then there's the growing queue. Each model in the chain sits behind a circuit breaker: after three consecutive failures it's skipped outright, so an outage stops costing you a timeout on every request. After thirty seconds a single probe call decides whether to reopen it. Health always comes from real traffic, never from background probes, which would cost money and burn the very rate limit you're trying to protect.&lt;/p&gt;

&lt;p&gt;What's left is spend. In the model profile you set unit prices and a spending cap per agent. Every response reports tokens and cost, and when the cap is exceeded, execution stops. You find out from your code, not from Monday's invoice.&lt;/p&gt;

&lt;p&gt;One caveat: a fallback model isn't equivalent to the primary one. Quality, latency and cost can all change. The chain is an explicit architectural choice, not a promise of identical results.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Monday morning: what did the agent do?
&lt;/h2&gt;

&lt;p&gt;A reliable system doesn't just react to errors. It lets you understand what happened.&lt;/p&gt;

&lt;p&gt;In ARA every response carries its own history. &lt;code&gt;AgentResponse.steps()&lt;/code&gt; holds the steps the runtime recorded, including tool calls with their arguments and results, even when the execution fails. In that case you get the partial trace, which is exactly what you need to see where it broke. If you use OpenTelemetry, ARA emits a span per execution, per LLM call and per tool dispatch.&lt;/p&gt;

&lt;p&gt;Two caveats. An operational trace records steps, calls and events, not the model's internal reasoning. And tracing doesn't remove the need to protect data: tool arguments and request contents still need to follow your own security and retention rules.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Why not just assemble this from Resilience4j and LangChain4j?"
&lt;/h2&gt;

&lt;p&gt;You can, and both are good libraries. Resilience4j gives you circuit breakers and retries; LangChain4j gives you the provider layer.&lt;/p&gt;

&lt;p&gt;The work is in the glue: deciding that a 429 should fail over but a 401 shouldn't, wiring a spend cap into the same call path, parking a tool call on a human decision without tying up a thread, and making sure validation runs before output leaves the agent. ARA is that glue in one place, built on top of LangChain4j rather than instead of it.&lt;/p&gt;

&lt;p&gt;If you'd rather own the glue, that's a fair choice. The four sections above are a decent checklist either way.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it without an API key
&lt;/h2&gt;

&lt;p&gt;One thing I care about a lot: being able to develop and test agents without depending on a remote provider.&lt;/p&gt;

&lt;p&gt;ARA includes &lt;code&gt;ScriptedLlmClient&lt;/code&gt;, a test client that replays predefined responses. It doesn't simulate a real model's intelligence. It lets you check the runtime's behavior repeatably, under controlled conditions.&lt;/p&gt;

&lt;p&gt;Add the dependency:&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;io.github.xmor&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;ara-runtime&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;1.0.3&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;And write:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AraRuntime is AutoCloseable: try-with-resources shuts it down cleanly&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AraRuntime&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AraRuntime&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="c1"&gt;// Stub client: replays a fixed script, one entry per LLM call&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;llmClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ScriptedLlmClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenFinalAnswer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Virtual threads are lightweight JVM threads."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;AraAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"assistant"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"You are a concise technical assistant."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// Runs the real execution loop; only the model's answer is canned&lt;/span&gt;
    &lt;span class="nc"&gt;AgentResponse&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;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentTask&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Explain virtual threads"&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;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No API key, no network, no cost. The runtime is the real one; only the answer is prepared in advance. The same approach works for tests: success paths, errors, tool calls, recovery conditions, without every test depending on an external provider. ARA tests itself this way.&lt;/p&gt;

&lt;p&gt;When you want a real model, add &lt;code&gt;ara-adapters&lt;/code&gt; and change one line:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pass this instead of ScriptedLlmClient: the rest of the code stays the same&lt;/span&gt;
&lt;span class="nc"&gt;LlmClient&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AraLlmClientFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openAi&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OPENAI_API_KEY"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You need Java 21 and Maven.&lt;/p&gt;
&lt;h2&gt;
  
  
  Don't take my word for it
&lt;/h2&gt;

&lt;p&gt;People tend to oversell their own projects. So here's where you can check every claim in this post yourself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Where to verify it in the repo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Output validated without spending tokens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AgentContract&lt;/code&gt;, &lt;code&gt;docs/CONTRACTS.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human approval the compiler forces you to handle&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ApprovalDecision&lt;/code&gt;, example &lt;code&gt;hitl/HumanInTheLoopExample&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failover only on retryable errors&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;FailoverLlmClient&lt;/code&gt;, &lt;code&gt;LlmException.isRetryable()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-agent spend cap&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LlmProfile&lt;/code&gt; (&lt;code&gt;costBudget&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A trace of every execution, even failed ones&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AgentResponse.steps()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests without real providers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ScriptedLlmClient&lt;/code&gt; and the &lt;code&gt;src/test&lt;/code&gt; folders&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And one number: the sources contain &lt;strong&gt;2,215 tests&lt;/strong&gt;, all run offline by &lt;code&gt;mvn verify&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What ARA is not (yet)
&lt;/h2&gt;

&lt;p&gt;I'd rather tell you the limits now than have you discover them later.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a library&lt;/strong&gt;, not a ready-to-install service. You integrate it into your application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't resume an interrupted execution.&lt;/strong&gt; There's no checkpoint to restart from yet, and pending approvals live in memory: if the process restarts, they're lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't design your security for you.&lt;/strong&gt; Authorization, tool idempotency and data handling stay your application's decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's young.&lt;/strong&gt; This is version 1.0.3, and the APIs will evolve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these is a blocker for you, open an issue. It's exactly the feedback that decides what comes next.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;ARA started in the evenings, after work. By day I'm a consultant, and I've seen enough systems to know the problem rarely ends when the code works in the ideal case.&lt;/p&gt;

&lt;p&gt;The project is built with heavy AI assistance, and I say so openly. Using AI to write code doesn't make design less important; it makes it more necessary. That's why the architectural rules are written down, dependencies are checked on every build, and every module has a test-coverage threshold that can only go up.&lt;/p&gt;

&lt;p&gt;AI speeds up the writing. It doesn't take over responsibility for the decisions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Next step
&lt;/h2&gt;

&lt;p&gt;ARA is an independent project with no company behind it. It grows only if developers who find it useful say so. Three things you can do, from quickest to most involved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Star the repo.&lt;/strong&gt; Ten seconds. It's how other Java developers discover the project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run an example.&lt;/strong&gt; Clone it and run &lt;code&gt;hitl/HumanInTheLoopExample&lt;/code&gt;: it runs offline with no API key, and in five minutes you see the 7:15 PM refund handled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell me what doesn't convince you.&lt;/strong&gt; A debatable architectural choice, a case ARA handles badly: open an issue.&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/xmor" rel="noopener noreferrer"&gt;
        xmor
      &lt;/a&gt; / &lt;a href="https://github.com/xmor/ara" rel="noopener noreferrer"&gt;
        ara
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      ARA — Agent Runtime Architecture: Java 21 framework for building autonomous AI agents and multi-agent systems. LLM integration, tool calling, deterministic I/O contracts, RAG and human-in-the-loop. On Maven Central (io.github.xmor).
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/xmor/ara/docs/assets/logo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxmor%2Fara%2FHEAD%2Fdocs%2Fassets%2Flogo.png" alt="ARA — Agent Runtime Architecture" width="320"&gt;&lt;/a&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;ARA — Agent Runtime Architecture for Java&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Build autonomous AI agents and multi-agent systems in plain Java 21.&lt;/strong&gt;
LLM integration, tool calling, deterministic I/O contracts, RAG and human-in-the-loop —
no annotations, no reflection, no Spring, no Kotlin.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://central.sonatype.com/artifact/io.github.xmor/ara-runtime" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b0c1e2af25836b81138847595e0c23e1b8921a3e91f1c8a9981094bf62e2640c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d6176656e25323043656e7472616c2d76312e302e332d626c75652e737667" alt="Maven Central"&gt;&lt;/a&gt;
&lt;a href="https://openjdk.org/projects/jdk/21/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/adc63ef3f21f6ff00bd790ddcd6fa545b8aae2d56d14bec90843ac7ff1fc7332/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a6176612d32312532422d6f72616e67652e737667" alt="Java"&gt;&lt;/a&gt;
&lt;a href="https://github.com/xmor/ara/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/109222cb0d1f59ed2e77b56722653623fa45f93e2bb201a6eef8561d26a52185/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d417061636865253230322e302d677265656e2e737667" alt="License"&gt;&lt;/a&gt;
&lt;a href="https://github.com/xmor/ara/commits/main" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8b6b00862a3fc0b2b2c1a5c175e3510c48277bbbb1d35a0c7483d2aebb744bc7/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f786d6f722f6172613f6c6162656c3d4c617374253230636f6d6d6974" alt="Last commit"&gt;&lt;/a&gt;
&lt;a href="https://github.com/xmor/ara/stargazers" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a946a284d4dbdaf61edb511f618965ddad99b0214c70cf4961467b15930a65bf/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f786d6f722f6172613f7374796c653d666c6174266c6162656c3d5374617273" alt="Stars"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/xmor/ara#quick-start--60-seconds-no-api-key" rel="noopener noreferrer"&gt;&lt;strong&gt;Quick start&lt;/strong&gt;&lt;/a&gt; ·
&lt;a href="https://github.com/xmor/ara#documentation" rel="noopener noreferrer"&gt;&lt;strong&gt;Documentation&lt;/strong&gt;&lt;/a&gt; ·
&lt;a href="https://github.com/xmor/ara#runnable-examples" rel="noopener noreferrer"&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/a&gt; ·
&lt;a href="http://ara.open-solutions.it" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Website&lt;/strong&gt;&lt;/a&gt; ·
&lt;a href="https://github.com/xmor/ara/discussions" rel="noopener noreferrer"&gt;&lt;strong&gt;Discussions&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;ARA is a &lt;strong&gt;Java 21 agent runtime&lt;/strong&gt;: a place where agents actually &lt;em&gt;run&lt;/em&gt;, with sessions
memory, execution strategies, cost budgets, approval gates and a full execution trace —
not a thin wrapper around a chat completion endpoint.&lt;/p&gt;
&lt;p&gt;It works with &lt;strong&gt;OpenAI, Anthropic, Ollama, Mistral, LM Studio, Groq&lt;/strong&gt; and any
OpenAI-compatible endpoint, and it is published on &lt;strong&gt;Maven Central&lt;/strong&gt; as
&lt;a href="https://central.sonatype.com/artifact/io.github.xmor/ara-runtime" rel="nofollow noopener noreferrer"&gt;&lt;code&gt;io.github.xmor:ara-runtime&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;Your code ──▶ AraRuntime ──▶ Agent (strategy + contract + session)
                                │
                     ┌──────────┼───────────┐
                     ▼          ▼           ▼
                 LlmClient   Tools      Retriever
             OpenAI/Claude/  (yours,   (in-memory
             Ollama/Mistral  parallel   or Qdrant)
                             on vthreads)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick start — 60 seconds, no API key&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. Add the dependency&lt;/strong&gt;…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/xmor/ara" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;An agent can work perfectly in a demo. Taking it to production means dealing with everything that happens once ideal conditions stop existing. That's why I built ARA.&lt;/p&gt;

</description>
      <category>java</category>
      <category>ai</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
