In the era of autonomous AI Agents, letting a Large Language Model (LLM) invoke backend tools directly presents a significant risk. If an Agent receives a command to "transfer $10,000 to user X" or "permanently purge the user database," executing it without human verification could result in financial or data disaster.
To bridge the gap between AI autonomy and enterprise safety, Solon AI (v4.0.5) introduces a robust, native Human-in-the-Loop (HITL) framework. By utilizing session-aware interceptors, it intercepts high-risk tool calls, suspends execution, and exposes an API for administrators to approve, reject, skip, or modify arguments before resuming.
This guide explores the design, API structure, and practical implementation of HITL workflows in Solon AI.
The HITL Architecture in Solon AI
The HITL architecture is designed around the ReAct (Reasoning and Acting) loop. Instead of letting the Agent execute tools immediately after reasoning, an interceptor chain checks the arguments against predefined safety rules.
[User Request]
│
▼
[ReActAgent] (Reasoning)
│
▼
[ToolCall] ────► [HITLInterceptor] (Evaluate Args)
│
┌─────────┴─────────┐
[Allowed / Safe] [High-Risk / Suspend]
│ │
▼ ├─► Push HITLPendingEvent
[Execute Tool] ├─► Set Session to PENDING
│ ▼
│ [Wait for Human Decision]
│ │
│ ▼ (via API Controller)
│ [HITL.submit(...) / approve / reject]
│ │
│ ├─► Push HITLDecidedEvent
│ ├─► Apply modifiedArgs / skip / reject
│ ▼
└───────────────► [Resume Execution]
At its core, Solon AI provides three main components:
-
HITLInterceptor: ReAct interceptor that registers target tools and runs strategies to decide if a request should be suspended. -
HITL: A static helper class offering convenient APIs for your controller to inspect pending tasks and submit human decisions. -
HITLDecision: An object containing the decision type (Approve, Reject, Skip), comments, and optional parameter modifications.
Step 1: Defining a High-Risk Tool
Let's assume we have a Java tool mapped to execute database transfers. In Solon AI, this is defined as a standard tool provider:
import org.noear.solon.ai.chat.tool.AbsToolProvider;
import org.noear.solon.ai.chat.tool.annotation.ToolMapping;
public class FinancialTools extends AbsToolProvider {
@ToolMapping(name = "transfer", description = "Transfer money to another account")
public String transfer(String targetAccount, double amount) {
// Execute the transfer transaction
System.out.printf("Successfully transferred $%.2f to %s\n", amount, targetAccount);
return "SUCCESS";
}
}
Step 2: Configuring the HITL Interceptor
An interceptor is attached to the ReActAgent during construction. We register our safety evaluation rules using the HITLStrategy interface:
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.agent.react.intercept.HITLInterceptor;
import org.noear.solon.ai.agent.react.intercept.HITLStrategy;
// Setup ReActAgent with HITL Interceptor
ReActAgent agent = ReActAgent.of(llmModel)
.defaultToolAdd(new FinancialTools())
.defaultInterceptorAdd(new HITLInterceptor()
// Define a strategy for the "transfer" tool
.onTool("transfer", (trace, args) -> {
double amount = Double.parseDouble(args.get("amount").toString());
// If the amount exceeds $5,000, trigger HITL interception
if (amount > 5000) {
return "The transfer amount exceeds the single-transaction limit of $5000.";
}
return null; // Safe to proceed without intervention
})
)
.build();
When the LLM outputs a tool call to transfer with an amount greater than 5000, the HITLInterceptor intercepts it, blocks execution, and tags the AgentSession as Pending.
Step 3: Handling Suspension in the Controller
When exposing the Agent through an HTTP endpoint, your controller must handle the PENDING state. Under the hood, Solon AI stores the task snapshots in the session context, which can be fetched using the HITL helper class.
import org.noear.solon.annotation.*;
import org.noear.solon.ai.agent.AgentSession;
import org.noear.solon.ai.agent.react.ReActResponse;
import org.noear.solon.ai.agent.react.intercept.HITL;
import org.noear.solon.ai.agent.react.intercept.HITLTask;
import org.noear.solon.core.handle.Result;
@Controller
@Mapping("/api/agent")
public class AgentController {
private final ReActAgent agent; // Injected or constructed
@Post
@Mapping("/ask")
public Result ask(String sessionId, String prompt) throws Throwable {
AgentSession session = getOrCreateSession(sessionId);
// Run the agent loop
ReActResponse resp = agent.prompt(prompt)
.session(session)
.call();
// If the execution was intercepted by HITL, return the pending task details
if (session.isPending()) {
HITLTask pendingTask = HITL.getPendingTask(session);
return Result.failure(403, "REQUIRED_HUMAN_APPROVAL", pendingTask);
}
return Result.succeed(resp.getContent());
}
}
If the agent decides to transfer $12,000, the REST API returns a status showing that approval is required, along with the HITLTask metadata (including the unique callUuid, the tool name, and the arguments).
Step 4: Submitting Human Decisions
Administrators need an endpoint to approve, reject, or bypass the transaction. With Solon AI v4.0.5, decisions are bound to a specific callUuid to handle parallel or batch tool calls.
Here is how you handle approval, rejection, skipping, and parameter modification:
import org.noear.solon.ai.agent.react.intercept.HITLDecision;
import java.util.Map;
@Post
@Mapping("/approve")
public Result approve(String sessionId, String callUuid, String action, @Body Map<String, Object> modifiedArgs) {
AgentSession session = getSession(sessionId);
// 1. Locate the precise task using callUuid
HITLTask task = HITL.getPendingTaskByCallUuid(session, callUuid);
if (task == null) {
return Result.failure("No pending task found for UUID: " + callUuid);
}
HITLDecision decision;
if ("approve".equalsIgnoreCase(action)) {
decision = HITLDecision.approve().comment("Approved by operations manager.");
// Human Override: If the manager adjusted the transfer amount down, apply it!
if (modifiedArgs != null && !modifiedArgs.isEmpty()) {
decision.modifiedArgs(modifiedArgs);
}
} else if ("reject".equalsIgnoreCase(action)) {
decision = HITLDecision.reject("Rejected: Suspected fraudulent activity.");
} else {
// Skip: Bypass this tool call, returning a custom mock message to the LLM
decision = HITLDecision.skip("Skipped: Submitter bypassed this step.");
}
// 2. Submit the decision back to the session
HITL.submit(session, task, decision);
// 3. Resume execution: Trigger agent call with a blank prompt
try {
ReActResponse resp = agent.prompt()
.session(session)
.call();
return Result.succeed(resp.getContent());
} catch (Throwable e) {
return Result.failure(e.getMessage());
}
}
Inside the Decision Lifecycle
When HITL.submit(...) is called, the decision is stored in the AgentSession context under _hitl_decision_<callUuid>. When the Agent resumes:
-
onAgentStart(trace): The interceptor intercepts the resume event. It scans all pending tasks in the session. If all have decisions, it sets the routing directly to theACTIONphase, skipping another LLM reasoning step. -
ActionTask: When executing the tool task:-
On Approve: The tool is invoked. If
modifiedArgsare provided, the interceptor clones the original arguments to preserve history, applies the modified parameters, and invokes the tool. - On Reject: The tool is blocked. A rejection message is injected into the exchange context as if the tool failed, steering the LLM to handle the denial gracefully.
-
On Skip: The tool is not run. The
commentfrom the decision is directly returned to the LLM as the mock result of the tool.
-
On Approve: The tool is invoked. If
Batch Tool Call & AlwaysAllow Settings
Solon AI's HITL framework also supports advanced features:
-
Batch Approvals: If the model decides to invoke three tools simultaneously,
HITL.getPendingTasks(session)returns a list of tasks. You can submit individual decisions usingHITL.submitAll(session, Map<callUuid, HITLDecision>)or approve them all withHITL.approveAll(session). -
alwaysAllow: In some cases, a manager wants to say "Approve this transfer, and trust this specific action for the rest of the session." By submittingHITLDecision.approve(true)(or.alwaysAllow(true)), the framework automatically registers a session-level rule to bypass subsequent checks for this specific tool.
Summary
The Human-in-the-Loop mechanism in Solon AI (v4.0.5) transforms risky AI operations into secure, auditable, and human-guided workflows. By placing interceptors right at the boundary of tool execution and enabling real-time argument overrides, Solon AI ensures that autonomous agents remain safely within corporate compliance guardrails.
Top comments (0)