Resume and approvals
Human in the loop is a first class primitive. Skills call wait_for_approval to pause the turn, the engine writes a pause_points row, and the chat surface shows an approval card. When the user approves, the resume endpoint unblocks the original turn from exactly where it left off.
Overview
- Tool
wait_for_approval- Module
src/lib/tools/providers/hitl.ts- Table
pause_points- Marker
__APPROVAL_REQUIRED__- Resume endpoint
POST /api/chat/resume- State after pause
- conversation.state = paused
Approvals are deliberately not a side channel. Treating HITL as a tool means it composes with everything else the engine already understands: it has a schema the model sees, an executor that runs server side, a tool result that the model reads on resume. No separate state machine, no parallel queue.
The wait_for_approval tool
A standard tool schema with a runtime executor that does one specific thing: write a pause point row and return a marker the chat UI renders as an approve/decline card.
pause_points schema
The durable record of an in-flight approval.
| State | Meaning | Next transition |
|---|---|---|
| pending | Awaiting user decision | approved, rejected, or expired |
| approved | User clicked approve | Engine resumes the turn |
| rejected | User clicked reject | Engine resumes the turn with a rejection result |
| expired | Pause point timed out | Engine resumes with a timeout error |
The approval marker
A short sentinel string the engine recognizes. It is the only side channel from the executor to the loop.
Resume endpoint
A small POST that records the decision and posts a synthetic user message back into the conversation. The next regular turn picks up naturally.
UI affordance
The chat surface renders a pause point as an inline approval card. The card shows what the skill wants to do, why, and lets the user approve, edit, or reject.
The approval card is just another message block. The chat renderer detects messages tied to a pause point id and swaps the default text block for the approval block. The user sees a structured card with the summary, the action payload, the risk level, and three buttons.
End to end flow
A complete approval cycle, from the skill calling the tool to the engine resuming.
- 01Skill calls wait_for_approvalThe skill exposes a structured summary, the action it wants to take, the payload, and a risk hint.
- 02Executor writes a pause pointThe executor inserts into
pause_pointsand returns the marker. - 03Engine detects the markerThe loop exits with stop reason pause. Conversation state moves to paused. The pause point id is stamped on the conversation.
- 04Chat surface renders the approval cardThe card shows the summary, the editable payload, and three buttons: approve, edit and approve, reject.
- 05User decidesThe browser posts to
/api/chat/resumewith the pause point id and the decision. - 06Engine posts a synthetic user messageThe resume handler writes a user-role message into the conversation summarizing the decision. The next chat turn the user takes (or an auto-continue if configured) reads that message and lets the model proceed.
File map
Glossary
- Pause point
- A row in pause_points capturing a turn that asked the user for approval. Exactly one per pause.
- Approval marker
- The sentinel string __APPROVAL_REQUIRED__<id> returned by the wait_for_approval tool. The engine treats it as a signal to exit the loop.
- Resume
- The act of accepting an approval decision and unblocking the original turn from where it paused.
- Payload override
- An optional edit to the proposed action a user can submit at approve time. The engine substitutes it into the tool result.
- Risk tone
- A low/medium/high label declared by the skill. The approval card surfaces it visually so the user weights the decision.