humanInTheLoopMiddleware, and gating the todoist agent
On a createAgent (module 2), you do not hand-write the interrupt node. You attach humanInTheLoopMiddleware.
import { humanInTheLoopMiddleware } from "langchain";
const agent = createAgent({
model, tools: [fileTask, closeTask, graduateTask], checkpointer,
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
closeTask: { allowedDecisions: ["approve", "edit", "reject"] },
graduateTask: { allowedDecisions: ["approve", "edit", "reject"] },
},
}),
],
});interruptOn names the tools that pause. allowedDecisions are the three outcomes: approve runs the tool call as-is; edit runs it with modified arguments (you change the labels or project before it fires); reject does not run it, and feeds the rejection back into the loop. fileTask is not listed, so it runs freely. closeTask and graduateTask pause. That is the exact policy your bridge wants: file auto-applies, destructive waits.
Try it yourself
edit vs reject
What is the concrete difference between edit and reject in terms of what the graph does next?
Show answer
Correct answer: C — edit runs the tool call with your modified arguments; reject skips it and feeds the rejection back
edit lets you change the tool call's arguments (e.g. swap the project or labels) before it actually executes. reject stops the tool call from running entirely, and the graph continues with that rejection as information rather than a completed action.
Middleware vs a hand-written interrupt node
This is a trade, not an upgrade. The middleware is not strictly better than the hand-rolled version.
What do you gain and lose using humanInTheLoopMiddleware instead of hand-writing interrupt()?
Reveal answer
You gain: the three-decision (approve/edit/reject) protocol built in, per-tool policy declared declaratively (interruptOn), and no need to write the pause/resume plumbing yourself. You lose: fine control over the exact interrupt payload shape and where in the loop the pause happens -- a hand-written interrupt() node lets you pause at an arbitrary point with an arbitrary custom payload, not just at tool-call boundaries.
Gate close and graduate behind interrupts
Extend the todoist agent with destructive powers, safely gated.
- Extend ~/dev/groundschool/builds/m2-todoist-agent with close and graduate tools.
- Add humanInTheLoopMiddleware with interruptOn for closeTask and graduateTask, allowedDecisions ["approve","edit","reject"].
- Wire the checkpointer from module 4 under this agent.
- Test all three resume paths: approve, edit (changing arguments before it fires), and reject.
- Kill the process after triggering an interrupt, then restart and resume the pending approval to prove it survives a restart.
All three HITL decisions resume correctly
Confirm approve, edit, and reject each produce the correct outcome.
approve executes the tool call as originally proposed; edit executes it with your modified arguments; reject does not execute it and the rejection is fed back into the loop. A pending approval also survives a process restart.