Human-in-the-Loop
Approval gates, escalation, and human oversight patterns
Human-in-the-Loop
Human-in-the-loop (HITL) is not a temporary workaround — it's a long-term architectural pattern. Agents that can pause, ask for approval, and escalate when uncertain are more trustworthy and capable than fully autonomous ones.
Tool Approval
The simplest HITL pattern: mark tools as requiring approval before execution.
const deleteRecords = Tool.create({
name: 'delete_records',
description: 'Delete records from the database',
parameters: z.object({
table: z.string(),
filter: z.record(z.string()),
}),
requiresApproval: true,
execute: async ({ table, filter }) => {
return await db.delete(table, filter)
},
})When the agent calls this tool, execution pauses and emits an approval event:
const stream = Runner.stream(agent, { messages })
for await (const event of stream) {
if (event.type === 'tool_approval_required') {
console.log(`Agent wants to call: ${event.name}`)
console.log(`Arguments: ${JSON.stringify(event.args)}`)
// Show to user, get decision
const approved = await askUser(`Allow ${event.name}?`)
if (approved) {
stream.approve(event.id)
} else {
stream.deny(event.id, 'User denied this action.')
}
}
}Conditional Approval
Require approval only under certain conditions:
const transferMoney = Tool.create({
name: 'transfer',
parameters: z.object({
amount: z.number(),
to: z.string(),
}),
// Only require approval for large amounts
requiresApproval: ({ amount }) => amount > 1000,
execute: async ({ amount, to }) => {
return await bank.transfer(amount, to)
},
})Human-as-Tool
The agent can explicitly ask a human for input when it's uncertain:
const askHuman = Tool.create({
name: 'ask_human',
description: 'Ask the human user a question when you need clarification or are uncertain',
parameters: z.object({
question: z.string().describe('The question to ask'),
context: z.string().describe('Why you need this information'),
options: z.array(z.string()).optional().describe('Suggested options, if applicable'),
}),
requiresApproval: false, // The tool itself IS the approval mechanism
execute: async ({ question, context, options }, toolContext) => {
// This is handled by the HITL integration
// The runner pauses and waits for the human response
return toolContext.requestHumanInput({ question, context, options })
},
})
const agent = new Agent({
name: 'cautious-assistant',
instructions: `You are a helpful assistant. When you're uncertain about something
or when a decision could have significant consequences, use ask_human to confirm
before proceeding.`,
tools: [webSearch, askHuman],
})Approval Policies
Define policies for systematic approval management:
import { ApprovalPolicy } from 'assistme-agent-sdk'
const policy = ApprovalPolicy.create({
rules: [
{
// Auto-approve read-only tools
match: { tools: ['web_search', 'read_file', 'list_files'] },
action: 'auto_approve',
},
{
// Require approval for write operations
match: { tools: ['write_file', 'send_email', 'delete_*'] },
action: 'require_approval',
},
{
// Auto-deny dangerous operations
match: { tools: ['drop_database', 'rm_rf'] },
action: 'auto_deny',
reason: 'This operation is not allowed.',
},
],
default: 'require_approval',
timeout: 300_000, // 5 minute timeout
onTimeout: 'deny', // Auto-deny if no response
})
const agent = new Agent({
name: 'managed',
model: claude('claude-sonnet-4-6'),
instructions: 'You are a helpful assistant.',
tools: [webSearch, readFile, writeFile, sendEmail],
approvalPolicy: policy,
})Escalation
When the agent can't complete a task, escalate to a human:
const agent = new Agent({
name: 'support',
model: claude('claude-sonnet-4-6'),
instructions: `You are a support agent. If you cannot resolve the issue within
3 attempts, escalate to a human agent.`,
tools: [searchDocs, createTicket],
hooks: {
onMaxTurns: async (context) => {
// Agent hit the turn limit — escalate
await notifyHuman({
channel: 'slack',
message: `Agent could not resolve: ${context.summary}`,
conversationId: context.runId,
})
return { output: 'I\'ve escalated this to a human agent who will follow up shortly.' }
},
},
})Web/Mobile Integration
For web and mobile apps, use the approval handler:
// Server-side
const stream = Runner.stream(agent, { messages })
const approvalManager = stream.approvalManager()
// When approval is needed, store it
approvalManager.onApprovalRequired(async (approval) => {
await db.insert('pending_approvals', {
id: approval.id,
runId: approval.runId,
tool: approval.name,
args: approval.args,
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
})
// Notify user via push notification, email, etc.
await notify(approval.userId, {
title: `Approval needed: ${approval.name}`,
body: JSON.stringify(approval.args),
})
})
// When user responds (e.g., via webhook or API call)
app.post('/api/approve/:id', async (req, res) => {
const { id } = req.params
const { approved, reason } = req.body
if (approved) {
await approvalManager.approve(id)
} else {
await approvalManager.deny(id, reason)
}
})Audit Trail
Every approval decision is recorded in the run trace:
const result = await stream.finalResult()
for (const entry of result.trace.approvals) {
console.log(`Tool: ${entry.tool}`)
console.log(`Args: ${JSON.stringify(entry.args)}`)
console.log(`Decision: ${entry.decision}`) // 'approved' | 'denied' | 'timed_out'
console.log(`By: ${entry.decidedBy}`) // User ID or 'policy'
console.log(`At: ${entry.decidedAt}`)
console.log(`Latency: ${entry.latencyMs}ms`)
}From Human-in-the-Loop to Human-on-the-Loop
As trust builds, shift from per-action approval to strategic oversight:
Phase 1: Human-in-the-loop
→ Agent asks permission for everything
→ User approves/denies each action
Phase 2: Policy-based automation
→ Define approval policies
→ Auto-approve routine, auto-deny dangerous
→ Only ask for edge cases
Phase 3: Human-on-the-loop
→ Agent operates autonomously within boundaries
→ Human reviews aggregate results periodically
→ Agent escalates only for novel situationsBest Practices
-
Show what will happen — Don't just say "Approve?". Show the exact action, arguments, and potential impact.
-
Set timeouts — Waiting forever for approval blocks the agent. Set reasonable timeouts with sensible defaults (auto-deny for dangerous ops, auto-approve for safe ones).
-
Store pending approvals — Don't rely on keeping the stream alive. Store approvals in a database so they survive disconnects.
-
Log everything — Every approval, denial, and timeout should be logged. This data is essential for building policies and building trust.
-
Start strict, loosen over time — Begin with
requiresApproval: trueon most tools. As you see patterns, create policies to auto-approve safe operations.