Skip to content

Commit 9e4969d

Browse files
feat(oss): hitl config expansion (#3818)
updating based on langchain-ai/langchain#37095
1 parent fe5363c commit 9e4969d

5 files changed

Lines changed: 121 additions & 18 deletions

File tree

‎src/oss/deepagents/human-in-the-loop.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ graph LR
1717
Human --> |approve| Execute
1818
Human --> |edit| Execute
1919
Human --> |reject| ToolMessage[ToolMessage]
20+
Human --> |respond| ToolMessage
2021
2122
Execute --> Agent
2223
ToolMessage --> Agent
@@ -36,7 +37,7 @@ graph LR
3637

3738
The `interrupt_on` parameter accepts a dictionary mapping tool names to interrupt configurations. Each tool can be configured with:
3839

39-
- **`True`**: Enable interrupts with default behavior (approve, edit, reject allowed)
40+
- **`True`**: Enable interrupts with default behavior (approve, edit, reject, respond allowed)
4041
- **`False`**: Disable interrupts for this tool
4142
- **`{"allowed_decisions": [...]}`**: Custom configuration with specific allowed decisions
4243

@@ -55,6 +56,7 @@ The `allowed_decisions` list controls what actions a human can take when reviewi
5556
- **`"approve"`**: Execute the tool with the original arguments as proposed by the agent
5657
- **`"edit"`**: Modify the tool arguments before execution
5758
- **`"reject"`**: Skip executing this tool call entirely
59+
- **`"respond"`**: Return the human's message directly as the tool result, skipping execution — for "ask user" style tools
5860

5961
You can customize which decisions are available for each tool:
6062

‎src/oss/langchain/frontend/human-in-the-loop.mdx‎

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ interface ActionRequest {
194194
}
195195

196196
interface ReviewConfig {
197-
allowedDecisions: ("approve" | "reject" | "edit")[];
197+
allowedDecisions: ("approve" | "reject" | "edit" | "respond")[];
198198
}
199199
```
200200

@@ -205,11 +205,11 @@ interface ReviewConfig {
205205
| `actionRequests[].args` | Structured arguments for the action |
206206
| `actionRequests[].description` | Optional human-readable description of what the action does |
207207
| `reviewConfigs` | Per-action configuration controlling which decisions are allowed |
208-
| `reviewConfigs[].allowedDecisions` | Which buttons to show: `"approve"`, `"reject"`, `"edit"` |
208+
| `reviewConfigs[].allowedDecisions` | Which buttons to show: `"approve"`, `"reject"`, `"edit"`, `"respond"` |
209209

210210
## Decision types
211211

212-
The HITL pattern supports three decision types:
212+
The HITL pattern supports four decision types:
213213

214214
### Approve
215215

@@ -259,9 +259,26 @@ const response: HITLResponse = {
259259
stream.submit(null, { command: { resume: response } });
260260
```
261261

262+
### Respond
263+
264+
The user provides a direct reply for "ask user" style tools. The `message` becomes the tool result and the tool itself is not executed:
265+
266+
```ts
267+
const response: HITLResponse = {
268+
decision: "respond",
269+
message: "Blue.",
270+
};
271+
272+
stream.submit(null, { command: { resume: response } });
273+
```
274+
275+
<Note>
276+
Use `respond` when the tool is intentionally a placeholder for human input — for example, an `ask_user` tool that prompts the agent to collect information from the user.
277+
</Note>
278+
262279
## Building the ApprovalCard
263280

264-
Here is a full approval card component that handles all three decision types:
281+
Here is a full approval card component that handles all four decision types:
265282

266283
```tsx
267284
function ApprovalCard({
@@ -276,7 +293,8 @@ function ApprovalCard({
276293
request.actionRequests[0]?.args ?? {}
277294
);
278295
const [rejectReason, setRejectReason] = useState("");
279-
const [mode, setMode] = useState<"review" | "edit" | "reject">("review");
296+
const [respondMessage, setRespondMessage] = useState("");
297+
const [mode, setMode] = useState<"review" | "edit" | "reject" | "respond">("review");
280298

281299
const action = request.actionRequests[0];
282300
const config = request.reviewConfigs[0];
@@ -320,6 +338,14 @@ function ApprovalCard({
320338
Edit
321339
</button>
322340
)}
341+
{config.allowedDecisions.includes("respond") && (
342+
<button
343+
className="rounded bg-purple-600 px-4 py-2 text-white"
344+
onClick={() => setMode("respond")}
345+
>
346+
Respond
347+
</button>
348+
)}
323349
</div>
324350
)}
325351

@@ -365,6 +391,25 @@ function ApprovalCard({
365391
</button>
366392
</div>
367393
)}
394+
395+
{mode === "respond" && (
396+
<div className="mt-4 space-y-2">
397+
<textarea
398+
className="w-full rounded border p-2"
399+
placeholder="Your response..."
400+
value={respondMessage}
401+
onChange={(e) => setRespondMessage(e.target.value)}
402+
/>
403+
<button
404+
className="rounded bg-purple-600 px-4 py-2 text-white"
405+
onClick={() =>
406+
onRespond({ decision: "respond", message: respondMessage })
407+
}
408+
>
409+
Send Response
410+
</button>
411+
</div>
412+
)}
368413
</div>
369414
);
370415
}
@@ -376,10 +421,12 @@ After the user makes a decision, the full cycle looks like this:
376421

377422
1. Call `stream.submit(null, { command: { resume: hitlResponse } })`
378423
2. The `useStream` hook sends the resume command to the LangGraph backend
379-
3. The agent receives the `HITLResponse` and continues execution
380-
4. If approved, the tool runs with the original (or edited) arguments
381-
5. If rejected, the agent receives the reason and decides its next step
382-
6. The `interrupt` property resets to `null` as the agent resumes streaming
424+
3. The agent receives the `HITLResponse` and continues execution. The HITL response may be one of:
425+
- `"approve"`: The agent continues executing the next action
426+
- `"reject"`: The agent receives the rejection reasoning and decides its next step
427+
- `"edit"`: The agent runs the tool with the edited arguments
428+
- `"respond"`: The human's message is returned directly as the tool result without executing the tool
429+
4. The `interrupt` property resets to `null` as the agent resumes streaming
383430

384431
<Tip>
385432
You can chain multiple HITL checkpoints in a single agent run. For example, an
@@ -396,6 +443,7 @@ with the results. Each interrupt is handled independently.
396443
| Financial transactions | `transfer_funds` | `["approve", "reject"]` |
397444
| File deletion | `delete_files` | `["approve", "reject"]` |
398445
| API calls to external services | `call_api` | `["approve", "reject", "edit"]` |
446+
| Collecting user input | `ask_user` | `["respond"]` |
399447

400448
## Handling multiple pending actions
401449

‎src/oss/langchain/human-in-the-loop.mdx‎

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ When a model proposes an action that might require review—for example, writing
77

88
It does this by checking each tool call against a configurable policy. If intervention is needed, the middleware issues an @[interrupt] that halts execution. The graph state is saved using LangGraph's [persistence layer](/oss/langgraph/persistence), so execution can pause safely and resume later.
99

10-
A human decision then determines what happens next: the action can be approved as-is (`approve`), modified before running (`edit`), or rejected with feedback (`reject`).
10+
A human decision then determines what happens next: the action can be approved as-is (`approve`), modified before running (`edit`), rejected with feedback (`reject`), or responded to directly (`respond`) for "ask user" style tools.
1111

1212
## Interrupt decision types
1313

14-
The [middleware](/oss/langchain/middleware/built-in#human-in-the-loop) defines three built-in ways a human can respond to an interrupt:
14+
The [middleware](/oss/langchain/middleware/built-in#human-in-the-loop) defines four built-in ways a human can respond to an interrupt:
1515

1616
| Decision Type | Description | Example Use Case |
1717
|---------------|---------------------------------------------------------------------------|-----------------------------------------------------|
1818
|`approve` | The action is approved as-is and executed without changes. | Send an email draft exactly as written |
1919
| ✏️ `edit` | The tool call is executed with modifications. | Change the recipient before sending an email |
2020
|`reject` | The tool call is rejected, with an explanation added to the conversation. | Reject an email draft and explain how to rewrite it |
21+
| 💬 `respond` | Tool execution is skipped; the human's message becomes the tool result. | Answer an "ask_user" prompt with a direct reply |
2122

2223
The available decision types for each tool depend on the policy you configure in `interrupt_on`.
2324
When multiple tool calls are paused at the same time, each action requires a separate decision.
@@ -47,7 +48,7 @@ agent = create_agent(
4748
middleware=[
4849
HumanInTheLoopMiddleware( # [!code highlight]
4950
interrupt_on={
50-
"write_file": True, # All decisions (approve, edit, reject) allowed
51+
"write_file": True, # All decisions (approve, edit, reject, respond) allowed
5152
"execute_sql": {"allowed_decisions": ["approve", "reject"]}, # No editing allowed
5253
"read_data": False, # Safe operation, no approval needed
5354
},
@@ -75,7 +76,7 @@ const agent = createAgent({
7576
middleware: [
7677
humanInTheLoopMiddleware({
7778
interruptOn: {
78-
write_file: true, // All decisions (approve, edit, reject) allowed
79+
write_file: true, // All decisions (approve, edit, reject, respond) allowed
7980
execute_sql: {
8081
allowedDecisions: ["approve", "reject"],
8182
// No editing allowed
@@ -120,7 +121,7 @@ const agent = createAgent({
120121
**`InterruptOnConfig` options:**
121122

122123
<ParamField body="allowed_decisions" type="list[string]">
123-
List of allowed decisions: `'approve'`, `'edit'`, or `'reject'`
124+
List of allowed decisions: `'approve'`, `'edit'`, `'reject'`, or `'respond'`
124125
</ParamField>
125126

126127
<ParamField body="description" type="string | callable">
@@ -473,6 +474,58 @@ When multiple actions are under review, provide a decision for each action in th
473474
```
474475
:::
475476

477+
</Tab>
478+
479+
<Tab title="💬 respond">
480+
Use `respond` for "ask user" style tools where the tool's real implementation is the human's reply. The `message` content is returned directly as the tool result; the tool itself is not executed.
481+
482+
:::python
483+
```python
484+
agent.invoke(
485+
Command(
486+
# Decisions are provided as a list, one per action under review.
487+
# The order of decisions must match the order of actions
488+
# in the interrupt request.
489+
resume={
490+
"decisions": [
491+
{
492+
"type": "respond",
493+
# The human's reply, returned directly as the tool result
494+
"message": "Blue.",
495+
}
496+
]
497+
}
498+
),
499+
config=config, # Same thread ID to resume the paused conversation
500+
version="v2",
501+
)
502+
```
503+
:::
504+
505+
:::js
506+
```typescript
507+
await agent.invoke(
508+
new Command({
509+
// Decisions are provided as a list, one per action under review.
510+
// The order of decisions must match the order of actions
511+
// in the interrupt request.
512+
resume: {
513+
decisions: [
514+
{
515+
type: "respond",
516+
// The human's reply, returned directly as the tool result
517+
message: "Blue.",
518+
}
519+
]
520+
}
521+
}),
522+
config // Same thread ID to resume the paused conversation
523+
);
524+
```
525+
:::
526+
527+
The `message` is returned to the agent as a successful `ToolMessage`. Use `respond` when the tool is intentionally a placeholder for human input—for example, an `ask_user` tool that prompts for clarification.
528+
476529
</Tab>
477530
</Tabs>
478531

@@ -567,7 +620,7 @@ The middleware defines an `after_model` hook that runs after the model generates
567620
2. The middleware inspects the response for tool calls.
568621
3. If any calls require human input, the middleware builds a `HITLRequest` with `action_requests` and `review_configs` and calls @[interrupt].
569622
4. The agent waits for human decisions.
570-
5. Based on the `HITLResponse` decisions, the middleware executes approved or edited calls, synthesizes @[ToolMessage]'s for rejected calls, and resumes execution.
623+
5. Based on the `HITLResponse` decisions, the middleware executes approved or edited calls, synthesizes @[ToolMessage]'s for rejected calls, returns human replies directly as @[ToolMessage]'s for `respond` decisions, and resumes execution.
571624

572625

573626

‎src/snippets/hitl-basic-config-js.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const agent = createDeepAgent({
5252
model: "google_genai:gemini-3.1-pro-preview",
5353
tools: [deleteFile, readFile, sendEmail],
5454
interruptOn: {
55-
delete_file: true, // Default: approve, edit, reject
55+
delete_file: true, // Default: approve, edit, reject, respond
5656
read_file: false, // No interrupts needed
5757
send_email: { allowedDecisions: ["approve", "reject"] }, // No editing
5858
},

‎src/snippets/hitl-basic-config-py.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ agent = create_deep_agent(
2525
model="google_genai:gemini-3.1-pro-preview",
2626
tools=[delete_file, read_file, send_email],
2727
interrupt_on={
28-
"delete_file": True, # Default: approve, edit, reject
28+
"delete_file": True, # Default: approve, edit, reject, respond
2929
"read_file": False, # No interrupts needed
3030
"send_email": {"allowed_decisions": ["approve", "reject"]}, # No editing
3131
},

0 commit comments

Comments
 (0)