Some actions are too important to leave to the model alone — sending an email, executing a transaction, modifying production data. Human-in-the-loop (HITL) lets you mark those actions and require human approval before they run. Cognis offers two paths to HITL, depending on whether you’re inside an agent loop or driving a graph directly.Documentation Index
Fetch the complete documentation index at: https://cognis.vasanth.xyz/llms.txt
Use this file to discover all available pages before exploring further.
Two layers
Cognis ships two distinct HITL surfaces — they pause at different points and answer different questions.| Layer | Purpose | API |
|---|---|---|
| Tool approval | Gate a tool call before dispatch. The most common HITL pattern. | Approver trait + AgentBuilder::with_approver(...) |
| Transcript-level pause | Pause every (or some) LLM call to consult a human — inject context, override the reply, or skip. | HumanInTheLoop middleware + HumanResponder trait |
| Graph interrupts | Pause a Graph<S> before/after named nodes; pull saved state, edit, resume. Useful for staged approvals at workflow boundaries. | with_interrupt_before / with_interrupt_after + resume |
Quick example — tool approval
AgentBuilder::with_approver wraps every registered tool in an ApprovalGatedTool. The approver decides per call.
Tools registered after
.with_approver(...) are NOT auto-wrapped. Call with_approver last.Decision covers the common shapes:
Decision::Approve— let the call run as-is.Decision::Reject { reason }— refuse; the model seesreasonas the tool’s output and can apologize or try something else. UseDecision::reject("nope")as a shortcut.Decision::Edit { args }— approve, but rewrite the args (e.g., cap a transfer amount).
cognis::tools: AutoApprove, RejectAll, AllowList — handy for tests or as a base to compose your own.
Quick example — graph interrupts
For workflows where you want to pause before (or after) a specific node, regardless of what the model decided:examples/graphs/graph_interrupts_demo.rs.
Transcript-level pause
For the rarer case where you want a human to potentially intervene on every model call (or a subset gated by a predicate), wrap the LLM client with theHumanInTheLoop middleware. It pauses before each call, asks a HumanResponder, and acts on a HumanDecision.
AgentBuilder-built agent (where the loop drives the LLM), wrap a PipelinedClient in a custom LLMProvider and feed that into Client::new(...). See Middleware → Wiring middleware into an agent.
Built-in HumanResponder impls under cognis::middleware: AlwaysSkip (no-op for testing). Closures that return HumanDecision work too via a blanket impl.
How it works
- The agent loop is graph-shaped. When a tool call is about to dispatch, control hits the approver before the dispatcher node runs.
- The graph engine treats interrupts as errors with structure.
CognisError::GraphInterruptedis not a failure — it’s a pause. Your code matches it, decides, and callsresume. - Checkpointers are required for
resume. The engine needs persisted state to restart from. See Graph workflows → Checkpointing. - Edits during pause are first-class.
graph.update_state(run_id, step, &edited)writes a new snapshot at the same step. Subsequentresumereads from it.
When to combine
Many production agents use both: anApprover for tool-call-level gating, and graph interrupts for higher-level workflow checkpoints (e.g., “pause before generating the final report so a reviewer sees the draft”). They don’t conflict; the approver runs inside the agent loop, the interrupt fires at the graph boundary you mark.
See also
Patterns → HITL approval
A complete approval flow with a CLI prompt.
Graph workflows → Checkpointing
What
resume reads from.Middleware
HumanInTheLoop, ApprovalGate, and friends.