or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced

error-handling.mdtype-inference.md
glossary.mdindex.mdquick-reference.mdtask-index.md
tile.json

hitl.mddocs/middleware/

Human-in-the-Loop Middleware

Pause agent execution for human review and approval of tool calls or model responses.

API Reference

/**
 * Create human-in-the-loop middleware
 * @param config - HITL configuration
 * @returns HITL middleware instance
 */
function humanInTheLoopMiddleware(
  config: HumanInTheLoopMiddlewareConfig
): AgentMiddleware;

interface HumanInTheLoopMiddlewareConfig {
  /**
   * When to interrupt for human review
   */
  interruptOn?: InterruptOnConfig;

  /**
   * Configuration for review interface
   */
  reviewConfig?: ReviewConfig;
}

Interrupt Configuration

/**
 * Configuration for when to interrupt
 */
type InterruptOnConfig =
  | "tools" // Interrupt before tool execution
  | "tool_results" // Interrupt after tool execution
  | ((state: State, toolCalls?: ToolCall[]) => boolean); // Custom logic

interface ReviewConfig {
  /**
   * Factory for generating action descriptions
   */
  descriptionFactory?: DescriptionFactory;
}

type DescriptionFactory = (
  action: Action,
  state: State
) => string | Promise<string>;

HITL Types

/**
 * Request for human review
 */
interface HITLRequest {
  /**
   * Action requiring review
   */
  action: Action;

  /**
   * Current agent state
   */
  state: State;

  /**
   * Description of the action
   */
  description?: string;
}

/**
 * Human decision response
 */
interface HITLResponse {
  /**
   * Decision made by human
   */
  decision: Decision;

  /**
   * Optional feedback or modifications
   */
  feedback?: string;
}

/**
 * Union of possible decisions
 */
type Decision = ApproveDecision | EditDecision | RejectDecision;

/**
 * Approve the action
 */
interface ApproveDecision {
  type: "approve";
}

/**
 * Edit and resubmit the action
 */
interface EditDecision {
  type: "edit";
  modifications: Record<string, any>;
}

/**
 * Reject the action
 */
interface RejectDecision {
  type: "reject";
  reason?: string;
}

/**
 * Action interface
 */
interface Action {
  type: "tool_call" | "model_response";
  payload: any;
}

/**
 * Action request interface
 */
interface ActionRequest extends Action {
  requestId: string;
  timestamp: number;
}

/**
 * Decision type enum
 */
type DecisionType = "approve" | "edit" | "reject";

Usage Examples

Basic HITL

import { createAgent, humanInTheLoopMiddleware, tool } from "langchain";
import { z } from "zod";

// Create tool that requires approval
const sendEmail = tool(
  async ({ to, subject, body }) => {
    await emailService.send({ to, subject, body });
    return `Email sent to ${to}`;
  },
  {
    name: "send_email",
    description: "Send an email",
    schema: z.object({
      to: z.string().email(),
      subject: z.string(),
      body: z.string(),
    }),
  }
);

// Create HITL middleware
const hitl = humanInTheLoopMiddleware({
  interruptOn: "tools", // Interrupt before tool execution
});

// Create agent with HITL
const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [sendEmail],
  middleware: [hitl],
  checkpointer: checkpointer, // Required for interrupts
});

// Start conversation
const result = await agent.invoke(
  {
    messages: [{ role: "user", content: "Send email to john@example.com" }],
  },
  {
    configurable: { thread_id: "thread-123" },
  }
);

// Agent will interrupt when tool is about to be called
// Resume after human approval
const resumed = await agent.invoke(
  {
    messages: [], // Empty to resume
  },
  {
    configurable: {
      thread_id: "thread-123",
      // Human decision goes here
    },
  }
);

Custom Interrupt Logic

import { humanInTheLoopMiddleware } from "langchain";

// Interrupt only for specific tools
const hitl = humanInTheLoopMiddleware({
  interruptOn: (state, toolCalls) => {
    // Review only dangerous operations
    const dangerousTools = ["delete_file", "send_email", "make_payment"];

    return (
      toolCalls?.some((call) => dangerousTools.includes(call.name)) ?? false
    );
  },
});

With Description Factory

import { humanInTheLoopMiddleware } from "langchain";

const hitl = humanInTheLoopMiddleware({
  interruptOn: "tools",
  reviewConfig: {
    descriptionFactory: async (action, state) => {
      if (action.type === "tool_call") {
        const { name, args } = action.payload;
        return `Agent wants to call ${name} with arguments: ${JSON.stringify(
          args,
          null,
          2
        )}`;
      }
      return "Action requires review";
    },
  },
});

Interrupt After Tool Results

import { humanInTheLoopMiddleware } from "langchain";

// Review tool results before continuing
const hitl = humanInTheLoopMiddleware({
  interruptOn: "tool_results",
});

// Use case: Verify API responses before agent acts on them
const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [apiTool],
  middleware: [hitl],
  checkpointer: checkpointer,
});

Multiple Decision Types

import { humanInTheLoopMiddleware, createAgent } from "langchain";

const hitl = humanInTheLoopMiddleware({
  interruptOn: "tools",
});

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [tool1, tool2],
  middleware: [hitl],
  checkpointer: checkpointer,
});

// Initial invocation - will interrupt
await agent.invoke(
  { messages: [{ role: "user", content: "Do something" }] },
  { configurable: { thread_id: "t1" } }
);

// Resume with approval
await agent.invoke(
  { messages: [] },
  {
    configurable: {
      thread_id: "t1",
      decision: { type: "approve" },
    },
  }
);

// Or resume with edit
await agent.invoke(
  { messages: [] },
  {
    configurable: {
      thread_id: "t1",
      decision: {
        type: "edit",
        modifications: { to: "different@email.com" },
      },
    },
  }
);

// Or resume with rejection
await agent.invoke(
  { messages: [] },
  {
    configurable: {
      thread_id: "t1",
      decision: {
        type: "reject",
        reason: "Insufficient information",
      },
    },
  }
);

Combining with Other Middleware

import {
  createAgent,
  humanInTheLoopMiddleware,
  toolCallLimitMiddleware,
  piiMiddleware,
} from "langchain";

// HITL with other safety middleware
const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [tools],
  middleware: [
    piiMiddleware({ builtInTypes: ["email", "credit_card"] }), // Check PII first
    toolCallLimitMiddleware({ runLimit: 5 }), // Limit tool calls
    humanInTheLoopMiddleware({ interruptOn: "tools" }), // Then human review
  ],
  checkpointer: checkpointer,
});

Best Practices

When to Use HITL

  • Financial transactions
  • Data deletion or modification
  • External communications (email, messages)
  • Access control changes
  • Any irreversible actions

Configuration

  • Use interruptOn: "tools" for preventive review
  • Use interruptOn: "tool_results" for verification
  • Use custom function for selective interrupts
  • Always provide a checkpointer

User Experience

  • Provide clear descriptions via descriptionFactory
  • Include relevant context in interrupts
  • Allow editing for minor corrections
  • Provide rejection feedback to agent

Error Handling

  • Handle rejected actions gracefully
  • Provide alternative paths for rejections
  • Log all human decisions
  • Implement timeouts for pending reviews

State Management

  • Use persistent checkpointers
  • Store thread IDs properly
  • Clean up old threads
  • Handle concurrent reviews carefully