11  Human-in-the-Loop

Human-in-the-loop (HITL) is where agent workflows stop being “cool demos” and start being trustworthy systems. This chapter introduces approvals and reviews as language primitives.

Now that you’ve seen how to structure an agent loop, HITL becomes a practical pattern: gate the risky step, collect missing inputs, and resume the workflow exactly where it left off.

11.1 The HITL Primitives

Tactus includes three blocking primitives you’ll use constantly:

  • Human.review({ ... }) — review an artifact (text, code, config) and optionally edit it
  • Human.approve({ ... }) — yes/no gate before a risky action
  • Human.input({ ... }) — request missing information the workflow shouldn’t guess

Each is a durable suspend point. When execution reaches one of these calls, the runtime:

  1. Checkpoints everything up to that point
  2. Emits a pending request to the human UI/handler
  3. Suspends execution
  4. Resumes later (even in a new process) when the human responds

That’s the difference between “agent scripts” and real workflows: you don’t keep a Python process running for 14 hours waiting for someone to click “Approve.”

11.2 Timeouts and “Fail Safe”

All blocking HITL calls can be configured with timeouts and defaults:

  • timeout = 3600 (seconds) to stop waiting eventually
  • default = false (or a default string, etc.) to pick a safe fallback

For risky actions, the safe default is almost always “don’t proceed.”

11.3 Example: Review the Draft, Then Approve Sending

The example for this chapter is code/chapter-10/50-meeting-recap-hitl-send.tac.

It follows a workflow shape you can reuse almost anywhere:

  1. Generate a structured draft (subject, body, action_items)
  2. Human.review(...) the draft (human can approve, reject, or revise)
  3. If revised, incorporate edits (or re-run the agent with feedback)
  4. Human.approve(...) before calling the send tool
  5. Call send_email(...) (stubbed here) and return message_id

Run it:

tactus run code/chapter-10/50-meeting-recap-hitl-send.tac \
  --param recipient_name="Sam" \
  --param recipient_email="sam@example.com" \
  --param raw_notes="Discussed Q1 launch timeline. Risks: vendor delays. Action: Sam to confirm dates by Friday."

In mock mode, HITL is automatically answered so tests can run without blocking:

tactus test code/chapter-10/50-meeting-recap-hitl-send.tac --mock

11.4 How HITL Shows Up in Different Environments

The same Lua code can run with different HITL handlers:

  • CLI: prompts in the terminal
  • IDE / desktop app: modal dialogs or inbox-style pending requests
  • Embedded platform: web UI, Slack, email, or your own integration

The procedure doesn’t change. That portability is the point.

This is also how you move beyond the “everyone uses chat” paradigm. Instead of forcing end users to steer the agent turn-by-turn, your application can surface structured, minimal interactions:

  • ask for one missing field at the moment it’s needed (not a long chat)
  • present a draft artifact in a review UI (with edits)
  • require an explicit approval before a side effect
  • show stages/logs so operators can monitor many runs at once

In other words: the human is still in the loop, but they’re an approver/reviewer/operator—not a full-time copilot.

11.5 Where This Takes Us

At this point in Part II, our workflow is:

  • Useful (draft + send integration)
  • Bounded and deterministic (safe loop shape)
  • Trustworthy (human gates before risky actions)

Next, Part III steps back and names the levers of control you have in an agent system—beyond prompts—then turns those into enforceable guardrails: capability control, sandboxing, isolation boundaries, and secretless execution. After that, Part IV covers reliability: specs and evaluations so you can trust the workflow over time.