Add Human-in-the-Loop Controls to an Agent SDK Agent
Add Human-in-the-Loop Controls to an Agent SDK Agent
Add HITL to an existing Agent SDK agent so it can pause high-stakes tool calls for human input
Add Human-in-the-Loop Controls to an Agent SDK Agent
Add HITL to an existing Agent SDK agent so it can pause high-stakes tool calls for human input
This recipe assumes you already have an agent built with the OpenRouter Agent
SDK and callModel. If you are starting from scratch, first read the
callModel overview to learn
about the Agent SDK.
Goal: Add human-in-the-loop (HITL) controls to an existing Agent SDK agent so one of its tools can auto-resolve routine decisions and pause for human input on high-stakes ones.
Outcome: Your existing callModel loop keeps running normally for routine
tool calls, pauses with status: 'awaiting_hitl' for high-stakes calls,
surfaces the pending call to your UI or API, and resumes after a human supplies
the tool result.
You can give this page to your coding agent as the implementation brief. It should adapt the example names, storage, threshold, and user-review surface to your existing agent rather than scaffold a separate app.
Both pause for human input, but they solve different problems:
Use HITL when the decision depends on the input data. Use requireApproval
when you need a human to approve whether a tool should execute. See the Tool
Approval & State
reference for details on approval flows and conditional approval predicates.
@openrouter/agent and callModelStateAccessor or a place to persist conversation statePick the tool in your existing agent where the result sometimes needs human judgment. In this example, the agent can approve small payments automatically but must pause before approving larger ones.
A HITL tool uses onToolCalled instead of execute. The hook receives the
parsed input and decides per-call whether to return a tool result immediately
or pause for a human.
Return a value to auto-resolve (like a regular tool). Return null to pause
the loop — the conversation status moves to 'awaiting_hitl' and the call
surfaces to the caller.
outputSchema is required for HITL tools — it validates both the
auto-resolved return value and any human-supplied response. See the
HITLTool type reference
for the full type signature.
When a human supplies a response for a paused call, onResponseReceived
fires before the result reaches the model. Use it to enrich, validate, or
transform the raw human input.
Use onResponseReceived when the human review surface does not return the
exact model-facing tool result you want. Common cases include:
reviewedAt, reviewerId, or an internal
approval ticket IDoutputSchemaReplace the tool definition from step 1 with this version:
If parsing or onResponseReceived throws, the error is surfaced to the model
as { error: ..., originalOutput: ... }. If omitted, the human-supplied value
passes through directly after schema validation.
Add the HITL tool to the tools array you already pass to callModel. HITL
resume requires conversation state, so reuse your existing StateAccessor or
add one if your agent is currently stateless.
The snippet below shows the minimum shape with in-memory state for clarity. In
production, back the StateAccessor with your database, Redis, or whatever
storage your agent already uses.
If you need a deterministic smoke test, temporarily force this tool call:
In production, your agent instructions or user request can let the model decide when to call the tool.
When the model invokes the tool and onToolCalled returns null, the result
pauses with status: 'awaiting_hitl'. Check the state after the call
completes, then surface the pending call to the human review surface in your
app.
Illustrative output shape:
Collect the human’s decision and resume by calling callModel again with a
function_call_output item for each paused call.
In the payment example, the human review surface could be as simple as:
In other HITL workflows, the human input might be more than a boolean. A
support escalation tool might collect an edited reply, a deployment tool might
collect a rollback plan, or a data-change tool might collect corrected field
values. Whatever collects the input should return a value that matches the
tool’s outputSchema.
The onResponseReceived hook fires on the human-supplied output before the
model sees it. In this example, it adds a reviewedAt timestamp.
status: 'awaiting_hitl'.pendingToolCalls contains the paused call with its id and arguments.function_call_output item continues the conversation.onResponseReceived transforms the human response before the model sees it.onToolCalled does not
require changes to the resume flow.