Build real AI agents

Human-in-the-loop

Gate risky tools behind approval

55 min5/7 in module

Learning objectives

  • Gate risky tools behind approval
  • Resume after approve/deny
  • Log the HITL decision in the trajectory

Production agents assume humans for irreversible actions

Demos auto-send emails and charge cards; production pauses before mutating tools until a human approves, edits, or denies. Human-in-the-loop (HITL) turns agent loops into resumable workflows: serialize state at the gate, surface proposed action, continue or abort based on decision, log everything in the trajectory.

This lesson gates ≥1 risky tool from lesson 7.2, implements approve/deny resume, and records HITL events for audit.

Callout — pending state is first-class: A waiting-for-approval run is not failure — persist checkpoint and resume without re-running prior tools.

Risk classification recap

From tool registry tags:

  • side_effect=mutating + requires_confirmation=True → HITL gate
  • Read-only search → no gate
  • Dry-run preview → optional auto-run; still log

Policy table in POLICY.md:

Tool Gate Timeout
create_ticket approve 24h
send_email approve 1h
search_kb none

Pause → approve → resume flow

Model proposes send_email(args)

Driver detects gate → status=PENDING_APPROVAL

Serialize checkpoint (messages, pending_call, run_id)

UI/CLI prompts human

approve → execute tool → append result → continue loop
deny  → append denial message → model recovers or exits

Checkpoint JSON example:

{
  "run_id": "run_abc",
  "step": 4,
  "messages": [...],
  "pending_tool": {"name": "send_email", "args": {...}, "call_id": "tc_1"},
  "status": "pending_approval"
}

Store in SQLite or checkpoints/ files keyed by run_id.

CLI approval pattern (course-friendly)

Agent proposes send_email:
  to: user@example.com
  subject: Refund processed
Approve? [y/n/e(edit)]: 
  • y — execute as proposed
  • n — inject tool result {"ok": false, "error": "denied_by_human"}
  • e — edit args JSON, then execute

Log decision:

{"event": "hitl", "decision": "approved", "actor": "cli_user", "tool": "send_email", "ts": "..."}

Resume without duplicate side effects

On approve, execute exactly once — idempotency key on mutating tools prevents double-send if user retries resume command.

If process crashes after approve but before execute, design execute_pending to be safe on replay.

Deny path: model should acknowledge and offer alternatives — test with eval prompt.

Callout — do not silently drop denials: Append explicit tool denial message so model context reflects human choice.

UX and latency realities

HITL async means runs span minutes/hours — not synchronous stdin only in production. Course CLI simulates; document how webhook/email approval would attach same checkpoint id.

Show pending runs: agent runs list --status pending.

Security

Approval UI must show full args — truncated subject lines hide CC abuse.

Authenticate approver — even locally use simple token or OS user name in log.

Async approval in real products

CLI stdin approval is a stand-in for:

  • Email link with signed token approving one pending action
  • Slack interactive button posting checkpoint summary
  • Admin dashboard queue of pending mutating tools

Checkpoint schema stays identical — only transport changes. Document resume --run-id run_abc --decision approved as analog of webhook callback.

Timeout and escalation

If approval not received within policy timeout:

  • Auto-deny mutating action and notify user
  • Or escalate to on-call queue for high-value workflows

Log timeout events separately from explicit deny — metrics differ (process issue vs intentional rejection).

Audit and compliance

HITL logs should answer: who approved, when, what args, which model version proposed it. Retention policy may require 90-day storage even for CLI demos — use append-only log file or SQLite hitl_audit table.

Redact PII in approval prompts displayed to operators unless role requires full view.

Deny path UX

When human denies, model should:

  • Acknowledge denial without retrying same mutating call blindly
  • Offer safe alternative (draft email instead of send)
  • Log final outcome even if task incomplete

Test deny path explicitly — teams over-test happy approve paths.

Edit-before-approve flow

Support e edit path updating args JSON before execute — common in production UIs (adjust email subject line). Log both original and edited args in HITL event for audit diff.

Edited args still pass Pydantic validation before tool runs.

Pending queue inspection

CLI command listing pending approvals helps operators:

agent pending list
agent pending approve run_abc

Even stub implementation with JSON files in checkpoints/ teaches resume UX before building web UI.

Notification hook (stretch)

Log pending HITL events to stdout with distinct prefix [HITL_PENDING] — grep-friendly for demos and future Slack webhook integration without changing checkpoint schema.

Batch approval (stretch)

When multiple pending runs exist, agent pending approve-all --before 2026-08-01 stub teaches bulk ops patterns — document why batch approve is dangerous for mutating tools without per-item review.

Testing HITL

  1. Propose mutating tool → assert loop pauses, no side effect yet.
  2. Approve → side effect occurs once, loop continues.
  3. Deny → no side effect, trajectory logs denial.
  4. Resume from checkpoint file after simulated restart.

Engineering problem (staff framing)

HITL is a control system: approve, edit, escalate — with SLAs so humans are not a silent bottleneck.

Diagram — HITL gate

flowchart LR
  Agent --> Risk{Risk score}
  Risk -->|low| Auto
  Risk -->|high| Human
  Human -->|approve| Act
  Human -->|reject| Abort

Precise definitions & mental model

Approval gates, queues, break-glass, feedback capture into evals.

Tradeoffs — when to use what

Safety vs latency/cost of human time.

Failure modes (interview + on-call)

Alert fatigue; no timeout policy; approvals without audit.

Production & OSS practices

Risk rubric; metrics on wait time; feed decisions to preference data.

Deep dive (FAANG / OSS bar)

Push «human-in-the-loop» past tutorial depth: write the interface contract (inputs/outputs/invariants), list three measurable metrics, and name two degrade modes if the happy path fails. Add a short threat note: what an attacker or noisy tool result could do, and which layer catches it (schema, policy, HITL, or eval gate).

flowchart LR
  Contract[Interface contract] --> Metrics
  Metrics --> Degrade[Degrade modes]
  Degrade --> Threat[Threat + control]

Micro-project: Approval before risky action

In m7/hitl/:

  1. Extend agent driver with checkpoint + pending state.
  2. Gate ≥1 mutating tool; CLI approve/deny/edit.
  3. Trajectory includes HITL events.
  4. README walkthrough: pending run, approve, completed run + deny run.

Checklist

  • Mutating tool blocked until approval
  • Checkpoint resume works after pause
  • Approve/deny logged in trajectory
  • Idempotency considered for execute path
Project checklist0/3 done

ShipAI delivery model is: