Human-in-the-Loop for Solon ReActAgent: Pause Risky Tools Before They Run Solon AI has introduced a Human-in-the-Loop (HITL) interceptor for its ReActAgent that pauses sensitive tool calls—such as money transfers or data deletions—before execution, allowing human review and correction without rebuilding the conversation. The feature uses official Solon APIs, including HITLInterceptor, HITLTask, and HITLDecision, and supports conditional pausing based on tool name or argument thresholds. This enables teams to keep low-risk automation fast while adding safety gates for dangerous operations. Most teams can get an AI agent to call tools. The hard part starts when the tool can move money, delete data, or send irreversible messages. That is where Human-in-the-Loop HITL stops being a slide-deck concept and becomes a product requirement. Solon AI ships this as a first-class interceptor on ReActAgent : the agent reasons and acts as usual, but sensitive tool calls can be paused, reviewed, corrected, and resumed without rebuilding the whole conversation. This post walks through a production-shaped pattern using only official Solon APIs current docs against Solon v4.0.3 . A naive design puts approval outside the agent: That is usually too late. Solon places HITL on the ReAct lifecycle itself: So the break point is exact: the model has already chosen transfer amount=2000 , but the tool has not executed yet. From the official HITL docs, the model is intentionally small: | Piece | Role | |---|---| HITLInterceptor | Declares which tools need review | HITLTask | Snapshot of tool name + args for the approver UI | HITLDecision | Approve / reject / skip + optional arg fixes | HITL | Business-layer helpers: getPendingTask , submit , approve , reject | You do not invent a custom implements Tool interface or wrap business agents in a fictional harness. Domain tools stay as AbsToolProvider + @ToolMapping . python import org.noear.solon.ai.annotation.ToolMapping; import org.noear.solon.annotation.Param; import org.noear.solon.ai.chat.tool.AbsToolProvider; public class FinanceTools extends AbsToolProvider { @ToolMapping description = "Query account balance by user id" public String get balance @Param description = "User id" String userId { return "{\"userId\":\"" + userId + "\",\"balance\":5200.00,\"currency\":\"CNY\"}"; } @ToolMapping description = "Transfer money to another account" public String transfer @Param description = "Source user id" String fromUserId, @Param description = "Target account" String toAccount, @Param description = "Amount" double amount { return "Transfer accepted: " + amount + " - " + toAccount; } } This matches the official domain-tool style used in the e-commerce after-sales sample: provider class + annotated methods, then defaultToolAdd ... . python import org.noear.solon.ai.agent.react.ReActAgent; import org.noear.solon.ai.agent.react.intercept.HITLInterceptor; import org.noear.solon.ai.chat.ChatModel; ChatModel chatModel = LlmUtil.getChatModel ; HITLInterceptor hitl = new HITLInterceptor // always pause these tools .onSensitiveTool "transfer" // or pause only when the amount crosses a threshold .onTool "transfer", trace, args - { double amount = Double.parseDouble args.get "amount" .toString ; return amount 1000 ? "Large transfer needs human approval" : null; } ; ReActAgent agent = ReActAgent.of chatModel .defaultToolAdd new FinanceTools .defaultInterceptorAdd hitl .maxTurns 10 .build ; Two useful patterns from the docs: onSensitiveTool ... onTool name, strategy null to let it passThat keeps low-risk automation fast while still fencing the dangerous path. python import org.noear.solon.annotation. ; import org.noear.solon.ai.agent.AgentSession; import org.noear.solon.ai.agent.react.ReActAgent; import org.noear.solon.ai.agent.react.ReActResponse; import org.noear.solon.ai.agent.react.intercept. ; import org.noear.solon.ai.agent.session.InMemoryAgentSession; import org.noear.solon.core.handle.Result; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Controller @Mapping "/ai/hitl" public class HitlWebController { private final Map