Human In The Loop
Implement human authorization approval gates, state persistence, and audit controls for high-stakes AI tool actions.
TL;DR
- Intercept sensitive tool requests using interactive supervisory
approval-gates. - Persist paused execution state in durable storage using a unique
checkpoint-id. - Resume suspended agent reasoning workflows upon receiving authenticated
human-confirmation.
Approval Gate Interception
Risk Tier ClassificationCategorize tools into automatic versus human-gated tiers.
const SENSITIVE_TOOLS = new Set([
'transfer_money',
'delete_database',
'send_external_email',
]);
const isGated = SENSITIVE_TOOLS.has(call.name);Pause Execution StateHalt agent execution and emit authorization request.
if (isGated) {
const token = await savePendingAction({
sessionId: session.id,
toolName: call.name,
args: call.args,
history: messages,
});
await notifySupervisor(token, call);
return { status: 'paused', approvalToken: token };
}Supervisor NotificationDispatch approval notification via Slack webhook or email.
await slack.postMessage({
channel: '#ai-approvals',
text: `Agent requests execution of: ${call.name}`,
attachments: [{ text: JSON.stringify(call.args) }],
});Durable State Persistence
Serialize Agent CheckpointStore pending execution state in Redis with expiration TTL.
await redis.setex(
`checkpoint:${approvalToken}`,
86400, // 24-hour expiration window
JSON.stringify({ history, toolCall })
);Resume State DeserializationRestore conversational context upon receiving human decision.
const key = `checkpoint:${token}`;
const raw = await redis.get(key);
if (!raw) throw new Error('Approval request expired');
const { history, toolCall } = JSON.parse(raw);
await redis.del(key);Audit Trail RecordLog human reviewer identity and approval timestamp.
await auditDb.insert({
tool: toolCall.name,
approvedBy: reviewer.email,
decision: 'APPROVED',
timestamp: new Date(),
});Webhook Decision Handlers
Approval Resume FlowExecute approved tool action and resume agent reasoning.
app.post('/api/approve', async (req, res) => {
const { token } = req.body;
const cp = await loadCheckpoint(token);
const out = await executeTool(cp.toolCall);
const ans = await resumeAgent(cp.history, out);
res.json({ success: true, answer: ans });
});Rejection Feedback FlowInform model of human refusal so it can adapt strategy.
app.post('/api/reject', async (req, res) => {
const { token, reason } = req.body;
const checkpoint = await loadCheckpoint(token);
const refusal = { error: `Denied: ${reason}` };
const ans = await resumeAgent(
checkpoint.history,
refusal
);
res.json({ success: true, answer: ans });
});Cryptographic HMAC VerificationVerify webhook signatures to prevent unauthorized bypass.
const signature = req.headers['x-signature'];
const expected = crypto.createHmac('sha256', SECRET)
.update(JSON.stringify(req.body)).digest('hex');
if (signature !== expected) {
throw new Error('Invalid HMAC');
}User Experience Patterns
Diff Preview GenerationDisplay side-by-side preview of intended data mutation.
function makeDiff(orig: string, mod: string) {
return diffLines(orig, mod);
// Renders visual green/red change preview in UI
}Timeout Auto-RejectionAutomatically cancel pending actions after inactivity window.
const age = Date.now() - checkpoint.created;
const isExpired = age > 3600000;
if (isExpired) {
await cancelPendingAction(checkpoint.id);
throw new Error('Approval window expired');
}Granular Parameter EditingAllow human supervisor to adjust tool parameters before run.
const editedArgs = supervisorUi.getModifiedInputs();
const out = await executeTool(call.name, editedArgs);Tips
- Assign risk tiers to tools: allow read operations to execute autonomously while gating write actions behind
approval-gates. - Persist conversational state in
redis-storageso agent execution can pause indefinitely without holding server memory.
Warnings
- Never allow agents to execute financial transactions or database updates without explicit
human-authorizationchecks. - Enforce cryptographic signature verification on approval webhooks to prevent spoofed
authorization-tokensfrom bypassing human checkpoints.
In Practice
Intercepts sensitive tool execution, saves checkpoint, and resumes workflow upon human confirmation.
- Check tool name against restricted high-risk tool set.
- Pause execution and store pending checkpoint state.
- Simulate human supervisor authorization decision.
- Resume agent execution with approved tool result.
const GATED = new Set(['delete_record', 'charge']);
interface Action { id: string; tool: string; }
const db = new Map<string, Action>();
function intercept(action: Action) {
if (GATED.has(action.tool)) {
db.set(action.id, action);
return { status: 'PAUSED', id: action.id };
}
return { status: 'EXECUTED' };
}
function approve(id: string) {
const act = db.get(id);
if (!act) throw new Error('Not found');
db.delete(id);
return { status: 'CONFIRMED', tool: act.tool };
}
console.log(intercept({ id: '1', tool: 'charge' }));
console.log(approve('1'));FAQ
HITL is an architectural pattern where an autonomous agent pauses its execution loop when encountering sensitive actions (like sending emails or transferring funds) to solicit explicit confirmation from a human supervisor before proceeding.
When a sensitive tool is requested, save the entire conversational history and pending tool call parameters to a durable database with a pending status. When the human approves via UI or webhook, reload the state and resume the loop.
Feed a rejection message into the conversation history as a tool_result (e.g., 'Action denied by supervisor'). This allows the model to acknowledge the refusal and suggest an alternative strategy.