Production Interceptors for Solon ReActAgent: Stop Loops, Retry Tools, Sanitize Observations, Stream Events Solon AI has introduced a set of built-in ReAct interceptors in v4.0.3 to address common production agent failures such as tool loops, flaky APIs, and oversized observations. The StopLoopInterceptor, ToolRetryInterceptor, and ToolSanitizerInterceptor provide guardrails for agent resilience, while the stream() method enables live UI feedback. Developers can wire these interceptors into a ReActAgent to create a production-ready agent without custom runtime code. Demo agents usually work once. Production agents fail in boring, expensive ways: they loop on the same tool call, they retry forever against a flaky API, and they paste a 40KB JSON blob back into the next thought. Solon AI already ships a small set of built-in ReAct interceptors for those failure modes. Pair them with stream event chunks, and you get both guardrails and live UI feedback without inventing a custom agent runtime. This article sticks to official Solon v4.0.3 APIs from the Agent docs. Not a bigger prompt. Not a fake harness wrapper. Three concrete controls: | Guardrail | Built-in interceptor | Job | |---|---|---| | Stop thrashing | StopLoopInterceptor | Break A-B-A-B tool loops | | Survive flaky tools | ToolRetryInterceptor | Physical retry + self-heal feedback | | Keep context clean | ToolSanitizerInterceptor | Truncate / desensitize observations | | Show progress | stream | Event chunks for thought / action / final answer | HITL and context compression are part of the same family, but they already have their own deep-dives. Today we assemble the resilience trio and wire a stream UI. Same pattern as the official after-sales sample: AbsToolProvider + @ToolMapping . No implements Tool . python import org.noear.solon.ai.annotation.ToolMapping; import org.noear.solon.ai.chat.tool.AbsToolProvider; import org.noear.solon.annotation.Param; public class OpsTools extends AbsToolProvider { @ToolMapping description = "Query order status by order id" public String get order @Param description = "Order id" String orderId { // Simulate occasional transport noise if Math.random < 0.3 { throw new RuntimeException "upstream timeout" ; } return "{\"orderId\":\"" + orderId + "\",\"status\":\"SHIPPED\",\"sku\":\"keyboard\"}"; } @ToolMapping description = "Fetch raw logistics payload can be large " public String get track @Param description = "Tracking number" String trackNo { StringBuilder sb = new StringBuilder "{\"trackNo\":\"" + trackNo + "\",\"events\": " ; for int i = 0; i < 200; i++ { if i 0 sb.append ',' ; sb.append "{\"ts\":" .append i .append ",\"msg\":\"hub-scan-" .append i .append "\"}" ; } sb.append " }" ; return sb.toString ; } } All five built-in interceptors live under org.noear.solon.ai.agent.react.intercept . For a default production baseline, start with these three: python import org.noear.solon.ai.agent.react.ReActAgent; import org.noear.solon.ai.agent.react.intercept.StopLoopInterceptor; import org.noear.solon.ai.agent.react.intercept.ToolRetryInterceptor; import org.noear.solon.ai.agent.react.intercept.ToolSanitizerInterceptor; import org.noear.solon.ai.chat.ChatModel; ReActAgent agent = ReActAgent.of chatModel .name "ops agent" .role "Operations assistant for order and logistics lookup" .defaultToolAdd new OpsTools .maxTurns 10 .autoRethink true // 1 break repeated action thrashing in a sliding window .defaultInterceptorAdd new StopLoopInterceptor 2, 6 // 2 retry flaky tool calls with linear backoff .defaultInterceptorAdd new ToolRetryInterceptor 3, 1000L // 3 truncate / clean oversized observations before they poison context .defaultInterceptorAdd new ToolSanitizerInterceptor 2000 .modelOptions o - o.temperature 0.1 .build ; | Class | Constructor | Meaning | |---|---|---| StopLoopInterceptor | maxRepeatCount, windowSize | In the last windowSize actions, the same action may appear at most maxRepeatCount times | ToolRetryInterceptor | maxRetries, retryDelayMs | Physical linear-backoff retries on tool failures; also supports logical self-heal feedback | ToolSanitizerInterceptor | maxObservationLength | Observation-stage truncate / denoise; optional custom Function