Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/src/createAgent/middleware/hitl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const writeFileTool = tool(

// Configure HITL middleware
const hitlMiddleware = humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
write_file: {
allowAccept: true,
description: "⚠️ File write operation requires approval",
Expand Down
24 changes: 12 additions & 12 deletions libs/langchain/src/agents/middlewareAgent/middleware/hitl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const contextSchema = z
* - `false` -> auto-approve (no human review)
* - `ToolConfig` -> explicitly specify which reviewer responses are allowed for this tool
*/
toolConfigs: z.record(z.union([z.boolean(), ToolConfigSchema])).default({}),
interruptOn: z.record(z.union([z.boolean(), ToolConfigSchema])).default({}),
/**
* Prefix used when constructing human-facing approval messages.
* Provides context about the tool call being reviewed; does not change the underlying action.
Expand Down Expand Up @@ -209,11 +209,11 @@ const contextSchema = z
* - `response`: Provide a manual response instead of executing the tool
*
* @param options - Configuration options for the middleware
* @param options.toolConfigs - Per-tool configuration mapping tool names to their settings
* @param options.toolConfigs[toolName].allowAccept - Whether the human can approve the current action without changes
* @param options.toolConfigs[toolName].allowEdit - Whether the human can reject the current action with feedback
* @param options.toolConfigs[toolName].allowRespond - Whether the human can approve the current action with edited content
* @param options.toolConfigs[toolName].description - Custom approval message for the tool
* @param options.interruptOn - Per-tool configuration mapping tool names to their settings
* @param options.interruptOn[toolName].allowAccept - Whether the human can approve the current action without changes
* @param options.interruptOn[toolName].allowEdit - Whether the human can reject the current action with feedback
* @param options.interruptOn[toolName].allowRespond - Whether the human can approve the current action with edited content
* @param options.interruptOn[toolName].description - Custom approval message for the tool
* @param options.messagePrefix - Default prefix for approval messages (default: "Tool execution requires approval"). Only used for tools that do not define a custom `description` in their ToolConfig.
*
* @returns A middleware instance that can be passed to `createAgent`
Expand All @@ -225,7 +225,7 @@ const contextSchema = z
* import { createAgent } from "langchain";
*
* const hitlMiddleware = humanInTheLoopMiddleware({
* toolConfigs: {
* interruptOn: {
* // Interrupt write_file tool and allow edits or accepts
* "write_file": {
* allowEdit: true,
Expand Down Expand Up @@ -304,7 +304,7 @@ const contextSchema = z
* Production use case with database operations
* ```typescript
* const hitlMiddleware = humanInTheLoopMiddleware({
* toolConfigs: {
* interruptOn: {
* "execute_sql": {
* allowAccept: true,
* allowEdit: true,
Expand Down Expand Up @@ -352,20 +352,20 @@ export function humanInTheLoopMiddleware(
/**
* Don't do anything if the last message isn't an AI message with tool calls.
*/
const lastMessage = messages
const lastMessage = [...messages]
.reverse()
.find((msg) => AIMessage.isInstance(msg)) as AIMessage;
if (!lastMessage || !lastMessage.tool_calls?.length) {
return;
}

if (!config.toolConfigs) {
throw new Error("HumanInTheLoopMiddleware: toolConfigs is required");
if (!config.interruptOn) {
throw new Error("HumanInTheLoopMiddleware: interruptOn is required");
}

// Resolve per-tool configs (boolean true -> all actions allowed; false -> auto-approve)
const resolvedToolConfigs: Record<string, ToolConfig> = {};
for (const [toolName, toolConfig] of Object.entries(config.toolConfigs)) {
for (const [toolName, toolConfig] of Object.entries(config.interruptOn)) {
if (typeof toolConfig === "boolean") {
if (toolConfig === true) {
resolvedToolConfigs[toolName] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe("humanInTheLoopMiddleware", () => {
model,
middleware: [
humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
calculator: true,
},
}),
Expand Down Expand Up @@ -150,7 +150,7 @@ describe("humanInTheLoopMiddleware", () => {
model,
middleware: [
humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
draft_email: true,
},
}),
Expand Down Expand Up @@ -222,7 +222,7 @@ describe("humanInTheLoopMiddleware", () => {
model,
middleware: [
humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
calculator: true,
},
}),
Expand Down Expand Up @@ -265,7 +265,7 @@ describe("humanInTheLoopMiddleware", () => {
model,
middleware: [
humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
calculator: true,
name_generator: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("humanInTheLoopMiddleware", () => {
it("should auto-approve safe tools and interrupt for tools requiring approval", async () => {
// Configure HITL middleware
const hitlMiddleware = humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
write_file: {
allowAccept: true,
description: "⚠️ File write operation requires approval",
Expand Down Expand Up @@ -212,7 +212,7 @@ describe("humanInTheLoopMiddleware", () => {

it("should handle edit response type", async () => {
const hitlMiddleware = humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
write_file: true,
},
});
Expand Down Expand Up @@ -280,7 +280,7 @@ describe("humanInTheLoopMiddleware", () => {

it("should handle manual response type", async () => {
const hitlMiddleware = humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
write_file: {
allowRespond: true,
},
Expand Down Expand Up @@ -349,7 +349,7 @@ describe("humanInTheLoopMiddleware", () => {

it("should throw if response is not a string", async () => {
const hitlMiddleware = humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
write_file: {
allowRespond: true,
},
Expand Down Expand Up @@ -413,7 +413,7 @@ describe("humanInTheLoopMiddleware", () => {

it("should allow to interrupt multiple tools at the same time", async () => {
const hitlMiddleware = humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
write_file: {
allowEdit: true,
description: "⚠️ File write operation requires approval",
Expand Down Expand Up @@ -516,7 +516,7 @@ describe("humanInTheLoopMiddleware", () => {

it("should throw if not all tool calls have a response", async () => {
const hitlMiddleware = humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
write_file: {
allowEdit: true,
description: "⚠️ File write operation requires approval",
Expand Down Expand Up @@ -583,7 +583,7 @@ describe("humanInTheLoopMiddleware", () => {

it("should not allow me to approve if I don't have allowAccept", async () => {
const hitlMiddleware = humanInTheLoopMiddleware({
toolConfigs: {
interruptOn: {
write_file: {
allowEdit: true,
description: "⚠️ File write operation requires approval",
Expand Down
Loading