Human in the loop
Human in the loop is implemented as a single tool. When a skill calls wait_for_approval, the executor writes a pause point row, returns a marker that signals the engine to halt, and lets the chat UI render an approve or decline card. The user's decision arrives on the next conversation turn through the resume endpoint.
Overview
- Module
src/lib/tools/providers/hitl.ts- Tool name
wait_for_approval- Table
pause_points- Marker
__APPROVAL_REQUIRED__<uuid>- Resume endpoint
POST /api/chat/resume- Status after pause
- conversation.state = paused
- Kinds
- approval, choice, data_entry
Treating approval as a tool keeps the surface uniform. The model already knows how to call tools; the engine already knows how to read tool results. Pausing for a human decision plugs into that flow rather than adding a parallel state machine. The pause point row is the only persisted artifact; everything else is derived from it.
When to use it
Call wait_for_approval before any action with material real-world consequences.
| Situation | Why approval matters |
|---|---|
| Sending an outbound email campaign | Mass communication that cannot be unsent |
| Finalising an agreement or contract | Legal commitment with downstream effects |
| Charging a card or creating an invoice | Financial side effect, possible chargeback exposure |
| Publishing content to a social account | Public statement attached to a brand voice |
| Scheduling a sequence of meetings | Calendar effects across multiple human recipients |
| Deleting records, files, or memory | Loss of state that cannot be recovered |
| Connecting or disconnecting an integration | Permission changes that affect later flows |
wait_for_approval as a reflex on every turn. Drafting, researching, and reviewing do not need approval. Calls that ship something or change state outside the platform do.Tool surface
Argument shape
Five fields. Two required, three optional.
| Field | Type | Notes |
|---|---|---|
title | string (<= 80 chars) | Short headline for the card. The first thing the user reads. |
description | string, markdown allowed | Full context the user needs to decide. Include counts, dollar amounts, recipients, reversibility. |
options | string[], up to 8 entries | Optional set of choices beyond approve and decline. When present, the card renders the choices in place of the default buttons. |
payload | object | Optional structured data echoed back through the resume endpoint. Use it to hold the action's job spec. |
kind | string | Category tag for logs and UI. Defaults to approval; use choice when options are supplied, data_entry for forms. |
The marker contract
The executor signals the engine by returning a sentinel string.
Pause point row
| status | meaning |
|---|---|
| pending | Awaiting user decision |
| resolved | User clicked approve, decline, or one of the options. resolution holds the chosen value. |
| expired | Pause point was abandoned. Engine ignores it on resume. |
The approval card
The chat surface renders a pending pause point as an inline card instead of plain text.
When the chat client receives a tool result whose text begins with the marker, it parses the trailing UUID, loads the pause point row, and renders an approval card in place of the raw text. The card shows the title, the description (markdown rendered), the editable payload if there is one, and either approve and decline buttons or the option list. The card stays visible until the user picks an action.
Resolution and resume
- 01User clicks an actionThe card posts the pause point id and the chosen resolution to the resume endpoint.
- 02Resume endpoint flips the rowStatus moves from pending to resolved and the resolution string is recorded.
- 03Synthetic user messageThe endpoint writes a user-role message into the conversation summarising the decision so the next turn picks it up naturally.
- 04Next turn proceedsThe chat handler reads the new message, the model sees the user has approved or declined, and the workflow continues.
Patterns
A few shapes that show up repeatedly.
Simple approval
Title plus description plus no options. The card renders Approve and Decline. Use this when the choice is binary: send or do not send, charge or do not charge.
Choice
Title plus description plus an options array. The card renders one button per option. Use this when the user is picking between several explicit alternatives, like a delivery date or a sequence variant.
Editable payload
Title plus description plus a payload object. The card shows the payload in an editor so the user can tweak the action before approving it. The edited payload is echoed through payloadOverride on resume.
Data entry
Set kind to data_entry when the card is collecting structured input rather than a yes or no. The UI renders a form; the resume endpoint stores the captured data as the resolution.
Anti-patterns
Things that look right but make the surface worse.
Audit trail
Every pause point keeps a record that survives the conversation.
The pause_points table is the canonical audit log for human decisions inside the workflow. Each row carries the conversation, the turn index, the kind, the resolution, and the resolution timestamp. Compliance reviews can join the table against the messages table to reconstruct who approved what and when, including any edits to the payload at resolution time.
File map
Glossary
- Pause point
- A row in pause_points capturing a turn that asked the user for approval. Exactly one per pause.
- Marker
- The sentinel string __APPROVAL_REQUIRED__<id> returned by the wait_for_approval executor. Triggers the engine to halt the loop.
- Resolution
- The user's chosen action. For a simple approval, approve or decline. For a choice, one of the options. For data entry, a structured value.
- Payload override
- An edit to the proposed action a user can submit at resolution time. Echoed back through the resume payload.
- Kind
- A category label that drives card rendering. Defaults: approval, choice, data_entry.
- Synthetic user message
- The message the resume endpoint writes back into the conversation summarising the decision. The next turn reads it like any other user input.