# ReActTrace in Solon AI: Turn Agent Runs into an Observable Boundary

> Source: <https://dev.to/solonjava/reacttrace-in-solon-ai-turn-agent-runs-into-an-observable-boundary-3lj0>
> Published: 2026-07-24 05:18:24+00:00

ReAct agents are easier to trust when you can answer more than “what did the model say?” You also need to know how many turns ran, which tools were called, whether a plan existed, and whether the run is waiting for approval.

Solon AI’s `ReActTrace`

is the runtime record for that job. In Solon v4.0.3, it acts as the state-machine context behind a ReAct run: short-term memory, route state, message sequence, plans, tool-call counts, metrics, and pending state live behind one trace.

A final answer is a poor production event. It tells you the outcome, but not whether the agent reached it efficiently or safely.

A useful operational record should answer:

`ReActTrace`

exposes these concerns directly:

```
ReActResponse response = agent.prompt("Investigate the failed payment and summarize the cause")
        .session(session)
        .call();

ReActTrace trace = response.getTrace();

System.out.println("steps=" + trace.getStepCount());
System.out.println("toolCalls=" + trace.getToolCallCount());
System.out.println("pending=" + trace.isPending());
System.out.println("pendingReason=" + trace.getPendingReason());
System.out.println("history=" + trace.getFormattedHistory());
```

The response also exposes `getMetrics()`

for execution indicators such as duration and token usage. Keep `getContent()`

for the user-facing answer; keep the trace and metrics for telemetry.

When planning is enabled, the plan is not merely prompt text. The trace can expose the current plan and its formatted progress:

```
ReActAgent agent = ReActAgent.of(chatModel)
        .name("payment_investigator")
        .planningMode(true)
        .maxTurns(8)
        .defaultToolAdd(new PaymentTools())
        .build();

ReActResponse response = agent.prompt("Investigate the failed payment")
        .session(session)
        .call();

ReActTrace trace = response.getTrace();
if (trace.hasPlans()) {
    trace.getPlans().forEach(step -> System.out.println("plan=" + step));
    System.out.println(trace.getPlanProgress());
}
```

This gives a UI or audit pipeline a stable place to show progress without parsing the model’s prose. The official API also provides `getFormattedPlans()`

when a formatted representation is more convenient.

For interactive applications, `stream()`

emits typed agent chunks. Solon’s documentation distinguishes planning, reasoning, thought, action, observation, and final ReAct chunks.

A UI can use those events as status signals:

```
agent.prompt("Check the latest payment status")
        .session(session)
        .stream()
        .doOnNext(chunk -> {
            if (chunk instanceof PlanChunk) {
                ui.showStatus("Planning");
            } else if (chunk instanceof ActionChunk) {
                ui.showStatus("Calling a tool");
            } else if (chunk instanceof ObservationChunk) {
                ui.showStatus("Processing tool result");
            } else if (chunk instanceof ReActChunk) {
                ui.showAnswer(chunk.getContent());
            }
        })
        .doOnError(ui::showError)
        .blockLast();
```

The important design choice is to render lifecycle status, tool names, and safe summaries—not to expose private reasoning text by default. Observability should help operators debug a run without turning internal deliberation into a user-facing contract.

Some workflows must pause before a sensitive action. `ReActTrace`

provides `pending(String)`

, `isPending()`

, and `getPendingReason()`

for a run that is waiting rather than finished.

That distinction matters to an API layer:

```
ReActResponse response = agent.prompt("Refund the customer")
        .session(session)
        .call();

ReActTrace trace = response.getTrace();
if (trace.isPending()) {
    return new AgentStatus("PENDING", trace.getPendingReason());
}

return new AgentStatus("COMPLETED", response.getContent());
```

Do not report a pending run as a successful empty answer. Persist the session according to your application’s policy, show the pending reason to an authorized reviewer, and resume through the documented session flow after the decision is available.

Keep framework-specific trace access in one adapter. This prevents controllers and dashboards from depending on every internal detail:

```
public record AgentRunSummary(
        String agent,
        int steps,
        int toolCalls,
        boolean pending,
        String pendingReason,
        String answer) {

    static AgentRunSummary from(ReActResponse response) {
        ReActTrace trace = response.getTrace();
        return new AgentRunSummary(
                trace.getAgentName(),
                trace.getStepCount(),
                trace.getToolCallCount(),
                trace.isPending(),
                trace.getPendingReason(),
                response.getContent());
    }
}
```

In production, I would add a correlation ID from the surrounding request, record duration and token metrics, and redact tool arguments or observations that contain secrets or personal data. `getFormattedHistory()`

is useful for controlled diagnostics, but it should not automatically become an unrestricted log line.

`getContent()`

to the user, not the raw trace.`isPending()`

as a separate state from success and failure.`maxTurns`

as a runtime guard even when planning is enabled.The useful mental model is simple: `ReActResponse`

is the result envelope; `ReActTrace`

is the run record. Once that boundary is explicit, agent observability becomes an engineering surface rather than a collection of prompt strings and console logs.

Official references:
