cd /news/ai-agents/solon-4-0-reactagent-a-practical-gui… · home topics ai-agents article
[ARTICLE · art-47503] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Solon 4.0 ReActAgent: A Practical Guide to Building AI Agents That Think and Act

Solon 4.0's ReActAgent enables developers to build AI agents that reason, use external tools, and adapt based on real-world feedback. The agent implements a cognitive loop of thought, action, and observation, and has been used in production for automated customer support and data analysis. A practical guide demonstrates creating tools and agents with the Solon framework.

read7 min views1 publishedJul 4, 2026

If you've ever wanted an AI that doesn't just chat but actually does things — queries databases, calls APIs, makes decisions, and learns from results — you're in the right place.

In this tutorial, I'll show you how to build production-ready AI agents using Solon 4.0's ReActAgent

. By the end, you'll have built an agent that can reason through complex problems, use external tools, and adapt its behavior based on real-world feedback.

Traditional LLMs are great at generating text, but they hit a wall when they need to interact with the real world — checking a database, fetching live data, or performing calculations.

ReActAgent

(Reason + Act) breaks through that wall. It implements a cognitive loop:

Thought → Action → Observation → (repeat or finish)

The agent thinks about what to do next, acts by calling a tool, observes the result, and decides whether to continue or deliver the final answer.

This isn't just theory. Solon's ReActAgent

has been used in production for automated customer support, intelligent data analysis, and multi-step workflow automation.

First, add the solon-ai-agent

module to your project:

<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-ai-agent</artifactId>
</dependency>

Note: If you're using Solon's parent POM, the version is managed automatically. Otherwise, use the latest Solon version.

Every agent needs a "brain" — a ChatModel

that powers reasoning. Let's build one using the fluent API:

import org.noear.solon.ai.chat.ChatModel;

ChatModel chatModel = ChatModel.of("https://api.moark.com/v1/chat/completions")
        .apiKey("your-api-key-here")
        .model("Qwen3-32B")
        .build();

You can also configure it via YAML and inject it:

solon.ai.chat:
  demo:
    apiUrl: "http://127.0.0.1:11434/api/chat"
    provider: "ollama"
    model: "llama3.2"
@Inject("${solon.ai.chat.demo}")
ChatConfig chatConfig;

ChatModel chatModel = ChatModel.of(chatConfig).build();

Let's start simple. Create a tool and a basic agent:

import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.annotation.Param;
import org.noear.solon.ai.chat.tool.AbsToolProvider;

import java.time.LocalDateTime;

// 1. Define a tool
public class TimeTool extends AbsToolProvider {
    @ToolMapping(description = "Get the current date and time")
    public String getCurrentTime() {
        return LocalDateTime.now().toString();
    }
}

// 2. Build and run the agent
public class HelloAgent {
    public static void main(String[] args) throws Throwable {
        ChatModel chatModel = ChatModel.of("https://api.moark.com/v1/chat/completions")
                .apiKey("***")
                .model("Qwen3-32B")
                .build();

        ReActAgent agent = ReActAgent.of(chatModel)
                .role("You are a helpful assistant that can check the time and date.")
                .defaultToolAdd(new TimeTool())
                .build();

        String response = agent.prompt("What time is it right now?")
                .call()
                .getContent();

        System.out.println(response);
    }
}

When you run this, the agent will:

getCurrentTime

tool."getCurrentTime()

.Let's build something more practical — a support agent that can query an order database and check inventory.

import org.noear.solon.ai.chat.tool.AbsToolProvider;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.annotation.Param;

public class OrderTool extends AbsToolProvider {

    @ToolMapping(description = "Query order status by order ID")
    public String getOrderStatus(@Param(description = "The order ID") String orderId) {
        // Simulate database lookup
        if ("ORD-1001".equals(orderId)) {
            return "Order ORD-1001: SHIPPED, estimated delivery July 7";
        } else if ("ORD-1002".equals(orderId)) {
            return "Order ORD-1002: PENDING, payment not confirmed";
        }
        return "Order not found: " + orderId;
    }

    @ToolMapping(description = "Check product inventory by product ID")
    public String checkInventory(@Param(description = "The product SKU") String sku) {
        // Simulate inventory check
        if ("SKU-A100".equals(sku)) {
            return "In stock: 42 units";
        } else if ("SKU-B200".equals(sku)) {
            return "Low stock: 3 units remaining";
        }
        return "Product not found: " + sku;
    }
}
ReActAgent supportAgent = ReActAgent.of(chatModel)
        .name("customer_support")
        .role("Customer Support Agent — you handle order inquiries and inventory checks.")
        .defaultToolAdd(new OrderTool())
        .maxTurns(8)                    // Max reasoning steps
        .autoRethink(true)              // Auto-rethink when stuck
        .retryConfig(3, 1000L)          // Retry 3 times, 1s delay
        .modelOptions(options -> {
            options.temperature(0.1);   // Low temperature for deterministic decisions
        })
        .build();

String result = supportAgent.prompt("Customer ORD-1002 wants to know when their order will arrive. Can you check?")
        .call()
        .getContent();

System.out.println(result);

The agent will:

ORD-1002

getOrderStatus("ORD-1002")

In production, you need visibility into what your agent is thinking. ReActInterceptor

gives you lifecycle hooks:

import org.noear.solon.ai.agent.react.ReActInterceptor;
import org.noear.solon.ai.agent.react.ReActTrace;
import org.noear.solon.ai.agent.react.task.ToolExchanger;

ReActAgent observableAgent = ReActAgent.of(chatModel)
        .name("observable_agent")
        .role("I help with various tasks.")
        .defaultToolAdd(new OrderTool())
        .defaultInterceptorAdd(new ReActInterceptor() {

            @Override
            public void onAgentStart(ReActTrace trace) {
                System.out.println("🤖 Agent started. Prompt: " + trace.getOriginalPrompt().getUserContent());
            }

            @Override
            public void onThought(ReActTrace trace, String thoughtContent,
                                   AssistantMessage assistantMessage) {
                System.out.println("💭 Thinking: " + thoughtContent);
            }

            @Override
            public void onAction(ReActTrace trace, ToolExchanger toolExchanger) {
                System.out.println("🛠️  Tool: " + toolExchanger.getToolName()
                        + ", args: " + toolExchanger.getArgs());
            }

            @Override
            public void onObservation(ReActTrace trace, ToolExchanger toolExchanger,
                                       ChatMessage observation, Throwable error,
                                       long durationMs) {
                if (error != null) {
                    System.err.println("❌ Tool failed: " + error.getMessage());
                } else {
                    System.out.println("✅ Tool result in " + durationMs + "ms");
                }
            }

            @Override
            public void onAgentEnd(ReActTrace trace) {
                System.out.println("✅ Agent finished.");
            }
        })
        .build();

This gives you a full audit trail of every decision your agent makes.

For long-running tasks, use stream()

to get real-time output:

agent.prompt("Analyze our top 10 products and give me a sales summary.")
     .stream()
     .doOnNext(resp -> {
         System.out.print(resp.getMessage().getContent());
     })
     .doOnComplete(() -> {
         System.out.println("\n✅ Analysis complete!");
     })
     .subscribe();

You can fine-tune behavior for individual calls using .options()

:

agent.prompt("Analyze this complex dataset and generate a JSON report.")
     .session(mySession)                // Reuse an existing session
     .options(o -> o
         .maxTurns(15)                  // More turns for complex tasks
         .planningMode(true)            // Enable planning phase
         .temperature(0.3)              // Balance creativity and precision
         .outputSchema("{\"type\":\"object\",\"properties\":{...}}")  // Structured output
         .toolAdd(new ReportingTool())  // Add temporary tool for this call
     )
     .call();
Category Method Description Default
Control maxTurns(int)
Max reasoning steps 8
Control autoRethink(boolean)
Enable auto-rethink false
Control retryConfig(int, long)
Retry count & delay 3, 1000ms
Model temperature(double)
Randomness (0-2) 0.5
Model max_tokens(long)
Max tokens to generate
Tools toolAdd(FunctionTool)
Add tool temporarily
Tools talentAdd(Talent)
Add talent/skill
Extension interceptorAdd(interceptor)
Add interceptor

ReActAgent

sessions enable long-running conversations with memory:

import org.noear.solon.ai.agent.session.InMemoryAgentSession;
import org.noear.solon.ai.agent.AgentSession;

// Create or reuse a session
AgentSession session = InMemoryAgentSession.of("user-session-123");

// First turn
String r1 = agent.prompt("Find me products under $50")
        .session(session)
        .call()
        .getContent();

// Second turn (agent remembers context)
String r2 = agent.prompt("What's the shipping time for the cheapest one?")
        .session(session)
        .call()
        .getContent();

// Inspect the trace
ReActTrace trace = agent.getTrace(session);
System.out.println("Total steps: " + trace.getStepCount());

// Or get a formatted summary
System.out.println(trace.getFormattedHistory());

The trace object gives you:

getFormattedHistory()

)getStepCount()

, getMetrics()

)getOriginalPrompt()

, getSession()

)Not all models support native tool calls. ReActAgent

supports Text ReAct mode — it uses regex to extract Action: {json}

tags from the model's text output. This makes it compatible with smaller, lighter models that don't have native tool-call support — perfect for edge deployments and cost-sensitive scenarios.

The execution style (ReActStyle

) is configured at build time through ReActAgentConfig

, choosing between ReActStyle.NATIVE

(OpenAI-style tool_calls, default) and the lightweight ReActStyle.TEXT

approach. When using Text mode, the agent parses Action: {json}

tags from the model's output and executes the corresponding tool.

Here's a full, copy-paste-ready example:

import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.agent.react.ReActInterceptor;
import org.noear.solon.ai.agent.react.ReActTrace;
import org.noear.solon.ai.agent.react.task.ToolExchanger;
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.annotation.Param;
import org.noear.solon.ai.chat.ChatModel;
import org.noear.solon.ai.chat.tool.AbsToolProvider;
import org.noear.solon.ai.chat.message.ChatMessage;

public class ECommerceSupportApp {
    public static void main(String[] args) throws Throwable {
        // 1. Build the model
        ChatModel model = ChatModel.of("https://api.moark.com/v1/chat/completions")
                .apiKey("${API_KEY}")
                .model("Qwen3-32B")
                .build();

        // 2. Build the agent
        ReActAgent agent = ReActAgent.of(model)
                .name("ecommerce_support")
                .role("E-commerce Support Agent")
                .defaultToolAdd(new OrderTool())
                .defaultToolAdd(new InventoryTool())
                .defaultInterceptorAdd(new LoggingInterceptor())
                .maxTurns(10)
                .autoRethink(true)
                .build();

        // 3. Run
        String answer = agent.prompt(
                "Customer wants to order SKU-A100 but saw ORD-1001 hasn't arrived yet. " +
                "Check both and explain the situation."
        ).call().getContent();

        System.out.println(answer);
    }
}

// Tools
class OrderTool extends AbsToolProvider {
    @ToolMapping(description = "Query order status by order ID")
    public String getOrderStatus(@Param(description = "Order ID") String orderId) {
        // Your database logic here
        return "ORD-1001: SHIPPED";
    }
}

class InventoryTool extends AbsToolProvider {
    @ToolMapping(description = "Check product inventory by SKU")
    public String checkStock(@Param(description = "Product SKU") String sku) {
        // Your inventory logic here
        return "SKU-A100: 42 units in stock";
    }
}

// Interceptor
class LoggingInterceptor implements ReActInterceptor {
    @Override
    public void onThought(ReActTrace trace, String thought,
                          AssistantMessage msg) {
        System.out.println("💭 " + thought);
    }
    @Override
    public void onAction(ReActTrace trace, ToolExchanger tool) {
        System.out.println("🛠️  " + tool.getToolName());
    }
}

@ToolMapping

and @Param

annotations — simple POJOs.Solon's ReActAgent

brings production-grade AI agent capabilities to the Java ecosystem with minimal boilerplate. The same framework design philosophy — restraint, efficiency, openness — applies here: you get powerful agent capabilities without framework lock-in.

Want to learn more? Check out the official Solon AI Agent documentation or explore the solon-ai GitHub repo.

── more in #ai-agents 4 stories · sorted by recency
── more on @solon 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/solon-4-0-reactagent…] indexed:0 read:7min 2026-07-04 ·