Skip to content

feat(langchain): add per-action human-in-the-loop middleware#10176

Open
pawel-twardziak wants to merge 7 commits intolangchain-ai:mainfrom
pawel-twardziak:feat/pa-hitl
Open

feat(langchain): add per-action human-in-the-loop middleware#10176
pawel-twardziak wants to merge 7 commits intolangchain-ai:mainfrom
pawel-twardziak:feat/pa-hitl

Conversation

@pawel-twardziak
Copy link
Contributor

@pawel-twardziak pawel-twardziak commented Feb 26, 2026

Summary

  • Adds perActionHumanInTheLoopMiddleware (aliased as paHitlMiddleware) - a variant of the existing humanInTheLoopMiddleware that processes tool call decisions independently rather than treating rejection as a batch-level abort
  • When any action is rejected, the existing HITL middleware discards all approved/edited actions and forces a jumpTo: "model" redirect. The new per-action variant instead executes approved/edited tool calls normally and converts only the rejected ones into synthetic ToolMessage(status="error") entries, letting the agent continue its tool execution flow
  • Reuses the same HITLRequest/HITLResponse interrupt contract and configuration shape (interruptOn, allowedDecisions, description, argsSchema) so it's a drop-in alternative

Motivation

The existing humanInTheLoopMiddleware treats a batch of tool calls as all-or-nothing: if any single action is rejected, the entire batch is discarded and control returns to the model. This is correct for strict oversight scenarios, but overly restrictive when an agent proposes multiple independent actions (e.g., a safe calculation alongside a risky file write). Users need a way to selectively approve/reject individual actions within the same interrupt without losing the approved work.

Key behavioral differences from humanInTheLoopMiddleware

┌────────────────────────────────────┬──────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────┐
│              Behavior              │         humanInTheLoopMiddleware         │                      perActionHumanInTheLoopMiddleware                      │
├────────────────────────────────────┼──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ Any rejection in batch             │ Discards all tool calls, jumpTo: "model" │ Executes approved/edited calls, rejected become ToolMessage(status="error") │
├────────────────────────────────────┼──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ Approved actions on partial reject │ Not executed                             │ Executed normally                                                           │
├────────────────────────────────────┼──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────┤
│ afterModel hook shape              │ { canJumpTo: ["model"], hook }           │ Plain async function (no jumpTo)                                            │
└────────────────────────────────────┴──────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────┘

Test plan

  • Unit tests covering: approve+reject mix, edit+reject mix, all-reject, tool call order preservation, decision count mismatch error, disallowed decision type error
  • Integration tests with createAgent + MemorySaver checkpointer: structured output flow with mixed decisions, edit+reject with checkpoint resume
  • Example script demonstrating the full interrupt-resume cycle with mixed approve/reject decisions

Diagrams

HITL:
https://link.excalidraw.com/readonly/hradCmix5ALfqpy9QlI8?darkMode=true

PA-HITL:
https://link.excalidraw.com/readonly/bNKuDF8Ry1z11AX21ouA?darkMode=true

Alternative solution

Add a no-batch-reject feature to the existing HITL middleware -> #10186

@changeset-bot
Copy link

changeset-bot bot commented Feb 26, 2026

🦋 Changeset detected

Latest commit: e1af613

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
langchain Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pawel-twardziak
Copy link
Contributor Author

Eric Lozano (@elozano98)

@elozano98
Copy link

Eric Lozano (elozano98) commented Mar 2, 2026

After testing it, I can confirm this solves the problem I was facing. Thanks pawel-twardziak!

@pawel-twardziak
Copy link
Contributor Author

and Christian Bromann (@christian-bromann) and Hunter Lovell (@hntrl) here is the python PR for this feature langchain-ai/langchain#32996

Comment on lines +161 to +166
const toolMessage = new ToolMessage({
content,
name: toolCall.name,
tool_call_id: toolCall.id!,
status: "error",
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tool message creation is ignoring if the tool message should also have some artifacts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm can tool message have artifacts?
I'll look at that. Thanks Eric Lozano (@elozano98) for spotting on this matter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this should be implemented here. Here's why:

  • This ToolMessage is a synthetic rejection message - it's created when a human rejects a tool call. The tool was never actually executed, so there are no artifacts to include. The message content is just a rejection notice (e.g., "User rejected the tool call for toolName").
  • The same pattern exists in the original HITL middleware - identical construction without artifacts. So this isn't something the PR introduced differently.
  • Artifacts on ToolMessage are typically used to attach structured data from actual tool execution results (e.g., binary data, files). For a rejection that never ran the tool, there's nothing meaningful to attach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants