Whetstone.
Human in the LoophumanInTheLoopMiddleware, and gating the todoist agent
Module 5, Lesson 225 min

humanInTheLoopMiddleware, and gating the todoist agent

On a createAgent (module 2), you do not hand-write the interrupt node. You attach humanInTheLoopMiddleware.

Gating specific tools with 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.

Practice

Try it yourself

Quiz

edit vs reject

What is the concrete difference between edit and reject in terms of what the graph does next?

  1. AThey're identical, both skip the action
  2. Bedit pauses the graph again, while reject resumes it immediately
  3. Cedit runs the tool call with your modified arguments; reject skips it and feeds the rejection back
  4. Dedit only works on file actions, reject only works on destructive actions
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.

Recall

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.

Do

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.
Done whenclose/graduate pause the graph with a readable approval payload, all three decisions (approve, edit, reject) resume correctly, and a pending approval survives a process restart.
Check

All three HITL decisions resume correctly

Confirm approve, edit, and reject each produce the correct outcome.

You should see

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.

Sign in to track your progress →