Build real AI agents
Human-in-the-loop
Gate risky tools behind approval
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 exitsCheckpoint 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_abcEven 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
- Propose mutating tool → assert loop pauses, no side effect yet.
- Approve → side effect occurs once, loop continues.
- Deny → no side effect, trajectory logs denial.
- 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/:
- Extend agent driver with checkpoint + pending state.
- Gate ≥1 mutating tool; CLI approve/deny/edit.
- Trajectory includes HITL events.
- 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
ShipAI delivery model is: