Agent SDK

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 situations

Best Practices

  1. Show what will happen — Don't just say "Approve?". Show the exact action, arguments, and potential impact.

  2. 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).

  3. Store pending approvals — Don't rely on keeping the stream alive. Store approvals in a database so they survive disconnects.

  4. Log everything — Every approval, denial, and timeout should be logged. This data is essential for building policies and building trust.

  5. Start strict, loosen over time — Begin with requiresApproval: true on most tools. As you see patterns, create policies to auto-approve safe operations.