christian-bromann

deepagents-hitl

Implementing human-in-the-loop approval workflows with interruptOn parameter for sensitive tool operations in Deep Agents.

christian-bromann 3 1 Updated 3mo ago
GitHub

Install

npx skillscat add christian-bromann/langchain-skills/deepagents-hitl

Install via the SkillsCat registry.

SKILL.md

deepagents-hitl (JavaScript/TypeScript)

Overview

HITL middleware adds human oversight to tool calls. Execution pauses for human decision: approve, edit, or reject.

Requires checkpointer to save state during interrupts.

Basic Setup

import { createDeepAgent } from "deepagents";
import { MemorySaver } from "@langchain/langgraph";

const agent = await createDeepAgent({
  interruptOn: {
    write_file: true,  // All decisions allowed
    execute_sql: { allowedDecisions: ["approve", "reject"] },  // No editing
    read_file: false,  // No interrupts
  },
  checkpointer: new MemorySaver()  // REQUIRED
});

Decision Table

Tool Type Config Decisions Use Case
Destructive true approve/edit/reject write_file, delete
Critical {allowedDecisions: [...]} approve/reject only deploy, SQL
Safe false none read_file

Code Examples

Example 1: Basic Approval

import { createDeepAgent } from "deepagents";
import { MemorySaver } from "@langchain/langgraph";
import { Command } from "@langchain/langgraph";

const agent = await createDeepAgent({
  interruptOn: { write_file: true },
  checkpointer: new MemorySaver()
});

const config = { configurable: { thread_id: "session-1" } };

// Step 1: Invoke (triggers interrupt)
let result = await agent.invoke({
  messages: [{ role: "user", content: "Write config to /prod.yaml" }]
}, config);

// Step 2: Check for interrupts
const state = await agent.getState(config);
if (state.next) {
  const interrupt = state.tasks[0];
  console.log("Interrupt:", interrupt);
}

// Step 3: Approve
await agent.updateState(config, {
  messages: [
    new Command({
      resume: {
        decisions: [{ type: "approve" }]
      }
    })
  ]
});

// Step 4: Continue
result = await agent.invoke(null, config);

Example 2: Edit Before Execution

const agent = await createDeepAgent({
  interruptOn: { execute_sql: true },
  checkpointer: new MemorySaver()
});

const config = { configurable: { thread_id: "session-1" } };

// Invoke
await agent.invoke({
  messages: [{ role: "user", content: "Delete old users" }]
}, config);

// Edit SQL
await agent.updateState(config, {
  messages: [
    new Command({
      resume: {
        decisions: [{
          type: "edit",
          args: {
            query: "DELETE FROM users WHERE last_login < '2020-01-01' LIMIT 100"
          }
        }]
      }
    })
  ]
});

// Continue
await agent.invoke(null, config);

Example 3: Reject with Feedback

const agent = await createDeepAgent({
  interruptOn: { deploy_code: true },
  checkpointer: new MemorySaver()
});

const config = { configurable: { thread_id: "session-1" } };

await agent.invoke({
  messages: [{ role: "user", content: "Deploy to production" }]
}, config);

// Reject
await agent.updateState(config, {
  messages: [
    new Command({
      resume: {
        decisions: [{
          type: "reject",
          message: "Tests haven't passed yet"
        }]
      }
    })
  ]
});

await agent.invoke(null, config);

Example 4: Custom Middleware

import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";

const agent = createAgent({
  model: "gpt-4",
  tools: [deployTool, sendEmailTool],
  middleware: [
    humanInTheLoopMiddleware({
      interruptOn: {
        deploy_to_prod: {
          allowedDecisions: ["approve", "reject"],
          description: "๐Ÿšจ PRODUCTION DEPLOYMENT requires approval"
        },
        send_email: {
          description: "๐Ÿ“ง Email draft ready for review"
        },
      },
    }),
  ],
  checkpointer: new MemorySaver(),
});

Boundaries

What Agents CAN Configure

โœ… Which tools require approval
โœ… Allowed decision types per tool
โœ… Custom interrupt descriptions
โœ… Checkpointer implementation

What Agents CANNOT Configure

โŒ HITL protocol structure
โŒ Skip checkpointer requirement
โŒ Interrupt without saving state

Gotchas

1. Checkpointer is REQUIRED

// โŒ Error
await createDeepAgent({ interruptOn: { write_file: true } });

// โœ… Must provide checkpointer
await createDeepAgent({
  interruptOn: { write_file: true },
  checkpointer: new MemorySaver()
});

2. Thread ID Required

// โŒ Can't resume without thread_id
await agent.invoke({...});
await agent.updateState(...);  // Which thread?

// โœ… Use consistent thread_id
const config = { configurable: { thread_id: "session-1" } };
await agent.invoke({...}, config);
await agent.updateState(config, ...);

3. Check State Between Invocations

// Interrupts happen between invoke() calls

// Step 1: invoke() -> interrupt
await agent.invoke({...}, config);

// Step 2: Check state
const state = await agent.getState(config);
if (state.next) {
  // Handle interrupts
}

// Step 3: Resume
await agent.updateState(config, {...});
await agent.invoke(null, config);

Full Documentation