Research date · May 6, 2026
Generated with AI

Human-in-the-Loop in AI Agent Frameworks

How LangChain, LangGraph, CrewAI, Google ADK, and Microsoft Semantic Kernel let a person step into the agent loop — to approve a tool call, edit a plan, sign off on the final answer, or own a long-running decision the agent simply cannot make alone.

The four places a human can step in

Strip away the framework labels and there are really only four moments in an agent's life where a human can — or must — be in the loop. Every framework in this deck supports some subset of these, with very different durability and developer-experience trade-offs.

LLM (reason) decide next action or final answer Tool (act) run the tool the LLM picked Final Answer return to user tool result · loop 1 2 3 4 Human approves · edits · responds · ignores (may take seconds, hours, or days) ↓ Inside the agent loop ↓ ↓ The human, on the other side ↓
1Tool-call approval
The LLM has picked a tool and arguments. Before the tool runs, a human is shown the call and may approve, edit, or reject. Used for irreversible or expensive actions (send email, place order, run shell).
2Mid-run pause + resume
The agent is mid-flight. It hits a checkpoint — explicitly via an interrupt() call, or because the framework was told "always pause before this step." State is persisted; a human responds at their leisure; the run resumes.
3Final-output review
The agent has produced a final answer (or draft email, or report). A human reviews and approves it before it leaves the building. Often plain pause-before-end.
4Long-running async wait
The agent calls a "tool" that is really a human inbox: an approval queue, a Slack message, a manual data-entry task. The framework needs to release the run and accept a callback hours or days later — without losing context.

How each framework actually does it

Each card: one big diagram of the framework's HITL machinery, then the four-pattern coverage and a code-grounded explanation underneath.

Lc

LangChain · the classic AgentExecutor

HITL is bolted on via callback handlers and "ask the human" tools — synchronous, in-process.
LLM Chain picks tool + args Tool.invoke() runs the picked tool HumanApprovalCallbackHandler on_tool_start callback prints call, asks y/n raises if rejected HumanInputRun (tool) a regular Tool the LLM can pick to ask the user a question (input() prompt) Human terminal y/n / prompt LLM picks "Human" intercepts approval / answer back
"Wrap tools in a callback that prompts y/n. Or hand the agent a 'Human' tool and let it ask."

How HITL works

LangChain has no first-class pause/resume primitive in AgentExecutor. Two patterns are common:

  • Callback approval — register HumanApprovalCallbackHandler (from langchain_community.callbacks.human). Its on_tool_start hook prints the proposed tool call and waits for a y/n; rejection raises an exception that the executor surfaces as a tool error.
  • "Human" as a tool — add a HumanInputRun tool to the agent's tool list. The LLM can choose to call it; the tool runs input() (or any custom prompt callable) to fetch a string from the user.

Both run synchronously, in-process. The agent thread is blocked while the terminal (or your custom callable) waits.

Pattern coverage

1 · Tool approval

Yes, via HumanApprovalCallbackHandler's on_tool_start hook.

2 · Pause + resume

No native primitive. The while loop blocks; restarting the process loses state.

3 · Final-output review

No built-in. You'd wrap the executor or post-process its output yourself.

4 · Long-running async

Possible only if your "Human" tool returns a future and you orchestrate the wait — not framework-managed.

Code anchor

Both classes ship in the langchain-community package (a separate repo from the core langchain repo, which only retains deprecation stubs):
· langchain_community.callbacks.human · HumanApprovalCallbackHandler
· langchain_community.tools · HumanInputRun

Lg

LangGraph · interrupt() + checkpointer = durable HITL

The only framework here with a true pause/resume primitive whose state survives a process restart.
StateGraph START agent (LLM) may call interrupt() inside the node body tool_calls? tools (ToolNode) interrupt_before=["tools"] → pause every tool call yes loop back with tool result END no Checkpointer (memory · SQLite · Postgres) { messages: [...], pending_writes: [...], interrupts: [{ value: ... }] } graph state is persisted at every super-step a process restart, hours later, can pick up exactly here → this is what makes interrupt() durable Client (any UI / queue / app) interrupt(value) raises GraphInterrupt → state checkpointed Command(resume=...) graph re-runs the node with resume value HumanInterrupt schema: accept · ignore · respond · edit interrupt resume
"Call interrupt() anywhere in any node. Persist state via the checkpointer. Resume hours later with Command(resume=value)."

How HITL works

LangGraph treats HITL as a first-class control-flow primitive. Anywhere in any node you can call interrupt(value) — it raises a GraphInterrupt, LangGraph snapshots the entire graph state via the configured checkpointer, and the value is surfaced to the calling client. A separate process — minutes, hours, or days later — can call the graph back with Command(resume=human_response); the runtime loads the snapshot and re-executes the node from the top, this time with the human's value returned by interrupt.

For "always pause before this node" semantics, compile the graph with interrupt_before=["tools"] or interrupt_after=["agent"]; this is how you do tool approval and final-output review without changing node code.

The HumanInterrupt typed dict — currently in langgraph.prebuilt.interrupt with a deprecation note that it will move to langchain.agents.interrupt — gives you a structured envelope (accept · ignore · respond · edit) that UIs can render consistently.

Pattern coverage

1 · Tool approval

interrupt_before=["tools"] on compile, or call interrupt() inside a custom tool node.

2 · Pause + resume

The native pattern. Pause is durable thanks to the checkpointer; resume via Command(resume=...).

3 · Final-output review

interrupt_after=["agent"] or call interrupt(draft) in a final node before return.

4 · Long-running async

Yes — checkpointer makes the wait survive process restarts. Approval can come from a totally different service.

Code anchor

libs/langgraph/langgraph/types.py · interrupt(), Command
libs/checkpoint{,-sqlite,-postgres} · checkpointers

Cr

CrewAI · task.human_input=True

A flag on the Task. After the agent finishes, a HumanInputProvider takes over and iterates feedback rounds.
Task description, expected_output agent = Researcher human_input = True opt-in HITL flag CrewAgentExecutor _invoke_loop() runs until AgentFinish AgentFinish draft answer HumanInputProvider .handle_feedback() .handle_feedback_async() while ask_for_human_input: prompt user for feedback inject as new message re-invoke the agent loop Human "looks good" / "fix X" / empty = accept Final answer → next task / user when human accepts (empty input) draft prompt feedback re-invoke loop with feedback
"Set a flag on the Task. The agent runs. Its answer is shown to the human. Empty input = accept. Anything else = re-run with that as feedback."

How HITL works

CrewAI's HITL is a per-task flag: Task(..., human_input=True). After CrewAgentExecutor._invoke_loop() returns an AgentFinish, the executor checks self.ask_for_human_input; if true, it calls _handle_human_feedback(), which delegates to a HumanInputProvider (the default SyncHumanInputProvider uses an input() prompt).

The provider runs a while ask_for_human_input loop: prompt for feedback, inject the feedback as a message, re-invoke the agent's loop, repeat. Empty feedback ends the cycle and the latest answer is the task's output. An async path (handle_feedback_async) exists for non-blocking I/O but still keeps the agent's loop in-process.

Pattern coverage

1 · Tool approval

No per-tool approval primitive. You'd wrap the tool yourself.

2 · Pause + resume

No durable pause; the provider blocks the in-process agent loop until input arrives.

3 · Final-output review

Native — this is exactly what human_input=True does, with iterate-on-feedback built in.

4 · Long-running async

The async provider helps, but state is in memory; a process restart loses it unless you persist out of band.

Code anchor

lib/crewai/src/crewai/task.py · human_input field
lib/crewai/src/crewai/agents/crew_agent_executor.py · _handle_human_feedback
lib/crewai/src/crewai/core/providers/human_input.py · provider impls

Ad

Google ADK · request_confirmation + long-running tools

Two complementary mechanisms: tool-call confirmation, and tools that release the run and resume on callback.
Path A · Tool-call confirmation LLM picks tool with require_confirmation request_confirmation processor flows/llm_flows/request_confirmation.py emits adk_request_confirmation call ToolConfirmation { confirmed: bool, payload: ... } @experimental Pydantic model Human approves / rejects in any client UI Path B · Long-running tool (durable async wait) LLM picks long-running tool e.g. get_user_choice LongRunningFunctionTool is_long_running = True framework records function_call_id and releases the run Session (Event log) pending function_call_id stored in events; durable across processes via SessionService Human / external answers later via function_response with matching id → run resumes callback with same function_call_id → resume Built-in: get_user_choice_tool — a LongRunningFunctionTool that asks the user to pick from a list of options. Compose with: request_confirmation processor on any tool by setting tool.require_confirmation.
"Two HITL channels: a confirmation request side-call for tool approvals, and 'long-running' tools that release the run and resume on callback."

How HITL works

Tool-call confirmation. Mark a tool to require confirmation; the request_confirmation request processor (in flows/llm_flows/request_confirmation.py) intercepts the call, emits a special adk_request_confirmation function call to the client carrying a ToolConfirmation envelope (confirmed, payload, hint). The client surfaces it to the human; their decision comes back as a function response and the original call either proceeds or is dropped.

Long-running tools. A LongRunningFunctionTool (or any tool with is_long_running=True) returns to the framework without a final result. ADK records the function_call_id in the Session as a pending event and releases the run. Whenever the external system (queue worker, ticketing tool, human via UI) is ready it sends a function response carrying the same id, and the agent resumes from that point. The built-in get_user_choice tool is implemented exactly this way.

Pattern coverage

1 · Tool approval

Native via request_confirmation + ToolConfirmation (currently @experimental).

2 · Pause + resume

Achievable via long-running tools — the run pauses and resumes on a function-response callback.

3 · Final-output review

No first-class hook, but easy to model as a long-running "review" tool the agent must call before finishing.

4 · Long-running async

Native. Persistence depends on which SessionService you use (in-memory vs. database).

Code anchor

src/google/adk/flows/llm_flows/request_confirmation.py
src/google/adk/tools/long_running_tool.py · LongRunningFunctionTool
src/google/adk/tools/get_user_choice_tool.py

Sk

Microsoft Semantic Kernel · auto-function-invocation filters

A filter sits in front of every plugin call the kernel auto-invokes — that's the HITL hook.
ChatCompletionAgent.invoke() ChatHistory user message assistant message + function_call + function_result Kernel FunctionChoiceBehavior.Auto model emits function_call → kernel must invoke AutoFunctionInvocation Filter async middleware: sees function_call_content await next(context) or set context.terminate=True or set context.function_result Plugin function @kernel_function runs only if filter calls await next(context) Human (prompted by your filter) CLI input · webhook reply · approval queue · … function_call await next filter awaits human decides
"Register a filter on the kernel. Every auto-invoked function passes through it. Approve, mutate, or terminate."

How HITL works

When FunctionChoiceBehavior.Auto is in effect, the kernel — not your code — invokes plugin functions on the model's behalf. The hook is a filter: register one with kernel.add_filter(FilterTypes.AUTO_FUNCTION_INVOCATION, my_filter). The filter is async middleware that receives an AutoFunctionInvocationContext (with the function_call_content, the chat_history, the request/function index in the loop, etc.) and decides what happens next.

Inside the filter you can: prompt a human (anything await-able will do — aioconsole.ainput, an HTTP wait, a queue subscriber); mutate the arguments before forwarding; set context.function_result to short-circuit the call entirely; or set context.terminate = True to stop the whole function-call loop. Calling await next(context) lets the call proceed.

Pattern coverage

1 · Tool approval

Native — auto-function-invocation filter is exactly this. Approve, mutate, or replace the result.

2 · Pause + resume

Process-level only — the filter awaits, but state is in memory. Restarting kills it.

3 · Final-output review

Done outside the kernel: review the agent's AgentResponseItem before forwarding. No built-in hook.

4 · Long-running async

Possible only if your filter integrates with a durable queue you own. Nothing built in.

Code anchor

python/semantic_kernel/filters/auto_function_invocation/auto_function_invocation_context.py · terminate, function_result
python/semantic_kernel/filters/filter_types.py · AUTO_FUNCTION_INVOCATION

Side-by-side · the four HITL patterns

Read down a column to see one framework's whole HITL story; read across a row to see how each framework answers the same need.

LangChain LangGraph CrewAI Google ADK Semantic Kernel
1 · Tool-call approval HumanApprovalCallbackHandler on on_tool_start interrupt_before=["tools"] at compile-time, or interrupt() in a tool node Not built in — wrap the tool yourself request_confirmation processor + ToolConfirmation Auto-function-invocation filter
2 · Mid-run pause + resume Not native — synchronous loop Native: interrupt(value) + Command(resume=…) Not native — the provider blocks the loop in-process Via long-running tool that awaits a function-response callback Filter can await in-process; not durable
3 · Final-output review No first-class hook interrupt_after=["agent"] or interrupt(draft) in a final node Native — exactly what task.human_input=True does Model as a long-running "review" tool the agent must call Done outside the agent (inspect AgentResponseItem)
4 · Long-running async wait Not framework-managed Native + durable — checkpointer survives process restarts Async provider exists, but state is in memory Native via LongRunningFunctionTool; durability = your SessionService Only if your filter is wired to a durable queue you own
Durability None (in-process) Memory · SQLite · Postgres checkpointers None (the executor must stay alive) Tied to SessionService impl (in-memory, DB, …) None (filter is in-process)
Human action vocabulary y/n (callback) or free-text (Human tool) Structured: accept · ignore · respond · edit (HumanInterrupt) Free-text feedback (empty = accept) Structured: { confirmed, payload, hint } Whatever your filter implements
Code anchor langchain_community/callbacks/human.py libs/langgraph/langgraph/types.py · interrupt lib/crewai/.../core/providers/human_input.py src/google/adk/flows/llm_flows/request_confirmation.py + tools/long_running_tool.py filters/auto_function_invocation/auto_function_invocation_context.py

Architect's cheat sheet

If HITL is non-negotiable in your domain (regulated industries, high-blast-radius actions, content moderation), the framework choice has consequences.

DURABILITY

1 · The hard question is "can the agent wait days?"

Most HITL "support" is really an input() blocking the agent thread. Only LangGraph (checkpointer) and ADK (long-running tool + persistent SessionService) survive a process restart with the run intact.

VOCABULARY

2 · Structured human responses are worth a lot

LangGraph's HumanInterrupt (accept · ignore · respond · edit) and ADK's ToolConfirmation are typed envelopes you can render consistently across UIs. CrewAI is free-text only; LangChain is y/n. UI-side reusability follows.

GRANULARITY

3 · Per-tool vs. per-step vs. per-task

ADK and SK gate at the tool / function level. LangGraph gates at the node / super-step level. CrewAI gates at the task level. Match the gate granularity to where your blast-radius lives.

DX

4 · Easiest path to "yes/no on every tool"

LangGraph: one line at compile (interrupt_before=["tools"]). ADK: mark tools with require_confirmation. SK: a 10-line filter that input()s. LangChain: register the callback. CrewAI: roll your own.

DX

5 · Easiest path to "review the final answer"

CrewAI wins here — human_input=True on the last task. LangGraph is close (interrupt_after=["agent"]). The others need wrappers.

RISK

6 · Don't conflate "filter that can prompt" with "real HITL"

A filter that awaits input() works on a laptop. Production HITL needs durability (graph state survives restarts), routability (any of N humans can answer), and auditability (who clicked yes?). Most of that is your problem regardless of the framework — but the framework should at least not make it impossible.