Human in the loop

1 min read
On trails:AGSAFEstep 4/7

Human in the loop (HITL) adds a pause or checkpoint to the agent’s decision making. For a risky action, the agent proposes and a person decides. It is the answer to financial and data-integrity riskProbabilistic risksThe model is probabilistic, so its failures are too: three threats, four things at stake.Opens in a new tab that no guardrailLLM guardrailsChecks around the model, so a bad input or a bad output is caught by something that does not rely on the model behaving.Opens in a new tab on the text can give.

The agent starts a high-risk action. A guardrail intercepts and pauses it and notifies a human. If the human approves, the action executes. If the human rejects, the agent is told and nothing runs. Agent high-risk action pause Guardrail intercept, pause state saved here notify Human review, decide approve Action executes reject: the agent is told, nothing runs
Approve runs the action. Reject sends the decision back to the agent.

Which actions pause

Not all of them. The orchestrator holds hard-coded rules: for example, a refund above 1000 needs a human, anything below goes straight through. The rule is code, so the model cannot argue its way around it. OWASP names the same control: require a person to approve high-impact actions, and the MCP specification says there should always be a human able to deny a tool call.

In LangGraph

interrupt() stops the run and surfaces a value. The graph’s state is saved by a checkpointer, and Command(resume=...) continues it with the human’s answer as the return value of interrupt().

Python
from typing import TypedDict from langgraph.checkpoint.memory import InMemorySaverfrom langgraph.graph import END, START, StateGraphfrom langgraph.types import Command, interrupt  class State(TypedDict):    amount: float    status: str  def refund(state: State) -> dict:    if state["amount"] > 1000:        approved = interrupt({"action": "refund", "amount": state["amount"]})        if not approved:            return {"status": "rejected"}    return {"status": "refunded"}  graph = (    StateGraph(State)    .add_node("refund", refund)    .add_edge(START, "refund")    .add_edge("refund", END)    .compile(checkpointer=InMemorySaver())) config = {"configurable": {"thread_id": "refund-1"}}paused = graph.invoke({"amount": 2500, "status": "new"}, config)  # stops at interrupt() graph.invoke(Command(resume=True), config)  # the human said yes

The paused run needs a checkpointer and a thread_id to find it again. LangChain also ships HumanInTheLoopMiddleware, where each tool is set to allow approve, edit, reject or respond.

The catch

Latency. A person is slow, and the run waits. It is the best of both worlds only for the few actions that are worth the wait.

The node restarts. On resume, LangGraph re-runs the node from the top, not from the interrupt() line. Any side effect placed before it should be safe to repeat, or it can run twice.


Sources