Build / AI /

Human In The Loop

Implement human authorization approval gates, state persistence, and audit controls for high-stakes AI tool actions.

TL;DR

  1. Intercept sensitive tool requests using interactive supervisory approval-gates.
  2. Persist paused execution state in durable storage using a unique checkpoint-id.
  3. Resume suspended agent reasoning workflows upon receiving authenticated human-confirmation.

Approval Gate Interception

    Risk Tier Classification

    Categorize 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 State

    Halt 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 Notification

    Dispatch 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 Checkpoint

    Store pending execution state in Redis with expiration TTL.

    await redis.setex(
      `checkpoint:${approvalToken}`,
      86400, // 24-hour expiration window
      JSON.stringify({ history, toolCall })
    );
    Resume State Deserialization

    Restore 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 Record

    Log 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 Flow

    Execute 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 Flow

    Inform 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 Verification

    Verify 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 Generation

    Display 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-Rejection

    Automatically 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 Editing

    Allow human supervisor to adjust tool parameters before run.

    const editedArgs = supervisorUi.getModifiedInputs();
    const out = await executeTool(call.name, editedArgs);

Tips

  1. Assign risk tiers to tools: allow read operations to execute autonomously while gating write actions behind approval-gates.
  2. Persist conversational state in redis-storage so agent execution can pause indefinitely without holding server memory.

Warnings

  1. Never allow agents to execute financial transactions or database updates without explicit human-authorization checks.
  2. Enforce cryptographic signature verification on approval webhooks to prevent spoofed authorization-tokens from bypassing human checkpoints.

In Practice

FAQ