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

> Source: <https://dev.to/solonjava/solon-40-reactagent-a-practical-guide-to-building-ai-agents-that-think-and-act-4ji8>
> Published: 2026-07-04 00:51:04+00:00

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:

``` python
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:

``` python
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.

``` python
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:

``` python
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:

``` python
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:

``` python
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.*
