interrupt_after interrupt before condition logic
Interrupt documentation makes no sense and doesn't work
How to interrupt a subgraph, and insert a message, and then rerun graph?
Before `interrupt_after`, is it necessary to first decide which node (execute the `path` function)?
Videos
I've spent the morning trying to implement LangGraph's interrupt function. It's unclear from any of the documentation how to actually do this. I've put in examples exactly how they are presented and none of it works.
Can anybody point to a working example of how to actually implement the interrupt feature to get human input during a graph? I just simply don't understand.
I'm an engineer on the LangChain team, and what follows is a copy & paste of my response to the same question posed as a GitHub issue on the LangGraphJS repository.
I haven't executed your code, but I think that the issue could be that on refusal you're not inserting a ToolMessage into the messages state. There are some docs on this here
You can handle this on refusal by returning a command with an update: field that has a tool message. For example:
async function approveNode (state) {
console.log('===APPROVE NODE===');
const lastMsg = state.messages.at(-1);
const toolCall = lastMsg.tool_calls.at(-1);
const interruptMessage = `Please review the following tool invocation:
${toolCall.name} with inputs ${JSON.stringify(toolCall.args, undefined, 2)}
Do you approve (y/N)`;
console.log('=INTERRUPT PRE=');
const interruptResponse = interrupt(interruptMessage);
console.log('=INTERRUPT POST=');
const isApproved = (interruptResponse.trim().charAt(0).toLowerCase() === 'y');
if (isApproved) {
return new Command({ goto: 'tools' });
}
// rejection case
return new Command({
goto: END,
update: {
messages: [
new ToolMessage({
status: "error"
content: `The user declined your request to execute the ${toolCall.name} tool, with arguments ${JSON.stringify(toolCall.args)}`,
tool_call_id: toolCall.id
}]
});
}
Also bear in mind that this implementation is not handling parallel tool calls. To handle parallel tool calls you have a few options.
- Decline to process all tool calls if the user disallows any tool call
- You'll need to add one rejection
ToolMessageper tool call, as shown above - In this model you might as well also only call
interruptonce for the whole batch of calls
- You'll need to add one rejection
- Allow approved calls to proceed without running denied calls:
- Two ways to do this:
- Option 1: Process all of the interrupts/approvals in a loop and return a Command that routes to
toolsif any calls are approved (orENDif no calls are approved)- To prevent the declined calls from processing, you'll want to use a
Sendobject in thegotofield and send a copy of theAIMessagewith the tool calls filtered down to just the approved list. - You'll still need the array of
ToolMessagein theupdatefield of the command as above - one for each declined call.
- To prevent the declined calls from processing, you'll want to use a
- Option 2: Use an array of
Sendin your conditional edge to fan out the tool calls to thetoolsnode (by sending a filtered copy of theAIMessage, as mentioned above) and do theinterruptin the tool handler.- Without
Sendhere you would wind up processing all tool messages in the same node, which would cause the approved tool calls to be reprocessed every time the graph is interrupted after that particular tool call is approved.
- Without
Here's a hastily-written example of how you could write a wrapper that requires approval for individual tool handlers for use with the "Option 2" approach mentioned in the last bullet above:
function requiresApproval<ToolHandlerT extends (...args: unknown[]) => unknown>(toolHandler: toolHandlerT) {
return (...args: unknown[]) => {
const interruptMessage = `Please review the following tool invocation: ${toolHandler.name}(${args.map(JSON.stringify).join", "})`;
const interruptResponse = interrupt(interruptMessage);
const isApproved = (interruptResponse.trim().charAt(0).toLowerCase() === 'y');
if (isApproved) {
return toolHandler(..args);
}
throw new Error(`The user declined your request to execute the ${toolHandler.name} tool, with arguments ${JSON.stringify(args)}`);
}
}
The issue is that rejecting a tool call without adding a ToolMessage breaks LangGraph's state assumptions. You're encoding approval logic into message-passing, which creates these edge cases.