langchain 1.5.10 → 1.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/_virtual/_rolldown/runtime.cjs +1 -1
  3. package/dist/agents/ReactAgent.cjs.map +1 -1
  4. package/dist/agents/ReactAgent.js.map +1 -1
  5. package/dist/agents/RunnableCallable.cjs.map +1 -1
  6. package/dist/agents/RunnableCallable.js.map +1 -1
  7. package/dist/agents/annotation.cjs.map +1 -1
  8. package/dist/agents/errors.cjs.map +1 -1
  9. package/dist/agents/middleware/constants.cjs.map +1 -1
  10. package/dist/agents/middleware/contextEditing.cjs.map +1 -1
  11. package/dist/agents/middleware/contextEditing.js.map +1 -1
  12. package/dist/agents/middleware/hitl.cjs +2 -1
  13. package/dist/agents/middleware/hitl.cjs.map +1 -1
  14. package/dist/agents/middleware/hitl.js +2 -1
  15. package/dist/agents/middleware/hitl.js.map +1 -1
  16. package/dist/agents/middleware/llmToolSelector.cjs +2 -1
  17. package/dist/agents/middleware/llmToolSelector.cjs.map +1 -1
  18. package/dist/agents/middleware/llmToolSelector.js +2 -1
  19. package/dist/agents/middleware/llmToolSelector.js.map +1 -1
  20. package/dist/agents/middleware/pii.cjs +17 -15
  21. package/dist/agents/middleware/pii.cjs.map +1 -1
  22. package/dist/agents/middleware/pii.js +17 -15
  23. package/dist/agents/middleware/pii.js.map +1 -1
  24. package/dist/agents/middleware/providerToolSearch.cjs.map +1 -1
  25. package/dist/agents/middleware/summarization.cjs +9 -2
  26. package/dist/agents/middleware/summarization.cjs.map +1 -1
  27. package/dist/agents/middleware/summarization.js +9 -2
  28. package/dist/agents/middleware/summarization.js.map +1 -1
  29. package/dist/agents/middleware/todoListMiddleware.cjs.map +1 -1
  30. package/dist/agents/middleware/toolCallLimit.cjs +5 -1
  31. package/dist/agents/middleware/toolCallLimit.cjs.map +1 -1
  32. package/dist/agents/middleware/toolCallLimit.js +5 -1
  33. package/dist/agents/middleware/toolCallLimit.js.map +1 -1
  34. package/dist/agents/middleware/toolEmulator.cjs +4 -2
  35. package/dist/agents/middleware/toolEmulator.cjs.map +1 -1
  36. package/dist/agents/middleware/toolEmulator.js +4 -2
  37. package/dist/agents/middleware/toolEmulator.js.map +1 -1
  38. package/dist/agents/middleware/toolError.cjs.map +1 -1
  39. package/dist/agents/middleware/utils.cjs.map +1 -1
  40. package/dist/agents/nodes/AgentNode.cjs +4 -1
  41. package/dist/agents/nodes/AgentNode.cjs.map +1 -1
  42. package/dist/agents/nodes/AgentNode.js +4 -1
  43. package/dist/agents/nodes/AgentNode.js.map +1 -1
  44. package/dist/agents/nodes/ToolNode.cjs +9 -7
  45. package/dist/agents/nodes/ToolNode.cjs.map +1 -1
  46. package/dist/agents/nodes/ToolNode.js +9 -7
  47. package/dist/agents/nodes/ToolNode.js.map +1 -1
  48. package/dist/agents/nodes/middleware.cjs.map +1 -1
  49. package/dist/agents/nodes/utils.cjs +2 -1
  50. package/dist/agents/nodes/utils.cjs.map +1 -1
  51. package/dist/agents/nodes/utils.js +2 -1
  52. package/dist/agents/nodes/utils.js.map +1 -1
  53. package/dist/agents/responses.cjs +1 -1
  54. package/dist/agents/responses.cjs.map +1 -1
  55. package/dist/agents/responses.d.cts.map +1 -1
  56. package/dist/agents/responses.d.ts.map +1 -1
  57. package/dist/agents/responses.js +1 -1
  58. package/dist/agents/responses.js.map +1 -1
  59. package/dist/agents/transformers/subagent.cjs.map +1 -1
  60. package/dist/agents/transformers/subagent.js.map +1 -1
  61. package/dist/agents/transformers/tool-call.cjs.map +1 -1
  62. package/dist/agents/transformers/tool-call.js.map +1 -1
  63. package/dist/agents/types.d.cts.map +1 -1
  64. package/dist/agents/types.d.ts.map +1 -1
  65. package/dist/agents/utils.cjs.map +1 -1
  66. package/dist/chat_models/universal.cjs +12 -10
  67. package/dist/chat_models/universal.cjs.map +1 -1
  68. package/dist/chat_models/universal.js +12 -10
  69. package/dist/chat_models/universal.js.map +1 -1
  70. package/dist/hub/base.cjs.map +1 -1
  71. package/dist/hub/base.js.map +1 -1
  72. package/dist/hub/index.cjs +2 -1
  73. package/dist/hub/index.cjs.map +1 -1
  74. package/dist/hub/index.js +2 -1
  75. package/dist/hub/index.js.map +1 -1
  76. package/dist/hub/node.cjs +4 -2
  77. package/dist/hub/node.cjs.map +1 -1
  78. package/dist/hub/node.js +4 -2
  79. package/dist/hub/node.js.map +1 -1
  80. package/dist/load/index.cjs.map +1 -1
  81. package/dist/tools/headless.cjs.map +1 -1
  82. package/package.json +18 -18
@@ -1 +1 @@
1
- {"version":3,"file":"hitl.js","names":[],"sources":["../../../src/agents/middleware/hitl.ts"],"sourcesContent":["/* oxlint-disable @typescript-eslint/no-explicit-any */\nimport { z } from \"zod/v3\";\nimport { AIMessage, ToolMessage, ToolCall } from \"@langchain/core/messages\";\nimport {\n InferInteropZodInput,\n interopParse,\n} from \"@langchain/core/utils/types\";\nimport { interrupt } from \"@langchain/langgraph\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport type { AgentBuiltInState, Runtime } from \"../runtime.js\";\nimport type { JumpToTarget } from \"../constants.js\";\nimport type { ToolCallRequest } from \"./types.js\";\n\nconst WhenFunctionSchema = z\n .function()\n .args(z.custom<ToolCallRequest<AgentBuiltInState>>()) // request\n .returns(z.union([z.boolean(), z.promise(z.boolean())]));\n\n/**\n * Predicate controlling whether a given tool call triggers an interrupt.\n *\n * Receives a {@link ToolCallRequest} and returns `true` to interrupt or `false`\n * to auto-approve the tool call.\n *\n * The request is constructed with `tool` set to `undefined` and `runtime` set to\n * the node-level {@link Runtime}, so it reflects the batch (`afterModel`) context\n * rather than a per-call tool execution.\n *\n * @param request - The tool call request being evaluated\n * @returns `true` to interrupt for the tool call, `false` to auto-approve it.\n * May also return a promise resolving to a boolean.\n *\n * @example\n * ```typescript\n * import { type WhenPredicate } from \"langchain\";\n *\n * // Only interrupt delete_file calls targeting /etc\n * const when: WhenPredicate = (request) =>\n * String(request.toolCall.args.path ?? \"\").startsWith(\"/etc\");\n * ```\n */\nexport type WhenPredicate = z.infer<typeof WhenFunctionSchema>;\n\nconst DescriptionFunctionSchema = z\n .function()\n .args(\n z.custom<ToolCall>(), // toolCall\n z.custom<AgentBuiltInState>(), // state\n z.custom<Runtime<unknown>>() // runtime\n )\n .returns(z.union([z.string(), z.promise(z.string())]));\n\n/**\n * Function type that dynamically generates a description for a tool call approval request.\n *\n * @param toolCall - The tool call being reviewed\n * @param state - The current agent state\n * @param runtime - The agent runtime context\n * @returns A string description or Promise that resolves to a string description\n *\n * @example\n * ```typescript\n * import { type DescriptionFactory, type ToolCall } from \"langchain\";\n *\n * const descriptionFactory: DescriptionFactory = (toolCall, state, runtime) => {\n * return `Please review: ${toolCall.name}(${JSON.stringify(toolCall.args)})`;\n * };\n * ```\n */\nexport type DescriptionFactory = z.infer<typeof DescriptionFunctionSchema>;\n\n/**\n * The type of decision a human can make.\n */\nconst ALLOWED_DECISIONS = [\"approve\", \"edit\", \"reject\"] as const;\nconst DecisionType = z.enum(ALLOWED_DECISIONS);\nexport type DecisionType = z.infer<typeof DecisionType>;\n\nconst InterruptOnConfigSchema = z.object({\n /**\n * The decisions that are allowed for this action.\n */\n allowedDecisions: z.array(DecisionType),\n /**\n * The description attached to the request for human input.\n * Can be either:\n * - A static string describing the approval request\n * - A callable that dynamically generates the description based on agent state,\n * runtime, and tool call information\n *\n * @example\n * Static string description\n * ```typescript\n * import type { InterruptOnConfig } from \"langchain\";\n *\n * const config: InterruptOnConfig = {\n * allowedDecisions: [\"approve\", \"reject\"],\n * description: \"Please review this tool execution\"\n * };\n * ```\n *\n * @example\n * Dynamic callable description\n * ```typescript\n * import type {\n * AgentBuiltInState,\n * Runtime,\n * DescriptionFactory,\n * ToolCall,\n * InterruptOnConfig\n * } from \"langchain\";\n *\n * const formatToolDescription: DescriptionFactory = (\n * toolCall: ToolCall,\n * state: AgentBuiltInState,\n * runtime: Runtime<unknown>\n * ) => {\n * return `Tool: ${toolCall.name}\\nArguments:\\n${JSON.stringify(toolCall.args, null, 2)}`;\n * };\n *\n * const config: InterruptOnConfig = {\n * allowedDecisions: [\"approve\", \"edit\"],\n * description: formatToolDescription\n * };\n * ```\n */\n description: z.union([z.string(), DescriptionFunctionSchema]).optional(),\n /**\n * JSON schema for the arguments associated with the action, if edits are allowed.\n */\n argsSchema: z.record(z.any()).optional(),\n /**\n * Optional predicate controlling whether to interrupt for a given tool call.\n *\n * Receives a {@link ToolCallRequest} and returns `true` to interrupt or\n * `false` to auto-approve the tool call.\n *\n * The request is constructed with `tool` set to `undefined` and `runtime` set\n * to the node-level {@link Runtime}, so `request.tool` is not available.\n *\n * @example\n * ```typescript\n * import type { InterruptOnConfig } from \"langchain\";\n *\n * // Only interrupt delete_file calls targeting /etc\n * const config: InterruptOnConfig = {\n * allowedDecisions: [\"approve\", \"reject\"],\n * when: (request) =>\n * String(request.toolCall.args.path ?? \"\").startsWith(\"/etc\"),\n * };\n * ```\n */\n when: WhenFunctionSchema.optional(),\n});\nexport type InterruptOnConfig = z.input<typeof InterruptOnConfigSchema>;\n\n/**\n * Represents an action with a name and arguments.\n */\nexport interface Action {\n /**\n * The type or name of action being requested (e.g., \"add_numbers\").\n */\n name: string;\n /**\n * Key-value pairs of arguments needed for the action (e.g., {\"a\": 1, \"b\": 2}).\n */\n args: Record<string, any>;\n}\n\n/**\n * Represents an action request with a name, arguments, and description.\n */\nexport interface ActionRequest {\n /**\n * The name of the action being requested.\n */\n name: string;\n /**\n * Key-value pairs of arguments needed for the action (e.g., {\"a\": 1, \"b\": 2}).\n */\n args: Record<string, any>;\n /**\n * The description of the action to be reviewed.\n */\n description?: string;\n}\n\n/**\n * Policy for reviewing a HITL request.\n */\nexport interface ReviewConfig {\n /**\n * Name of the action associated with this review configuration.\n */\n actionName: string;\n /**\n * The decisions that are allowed for this request.\n */\n allowedDecisions: DecisionType[];\n /**\n * JSON schema for the arguments associated with the action, if edits are allowed.\n */\n argsSchema?: Record<string, any>;\n}\n\n/**\n * Request for human feedback on a sequence of actions requested by a model.\n *\n * @example\n * ```ts\n * const hitlRequest: HITLRequest = {\n * actionRequests: [\n * { name: \"send_email\", args: { to: \"user@example.com\", subject: \"Hello\" } }\n * ],\n * reviewConfigs: [\n * {\n * actionName: \"send_email\",\n * allowedDecisions: [\"approve\", \"edit\", \"reject\"],\n * description: \"Please review the email before sending\"\n * }\n * ]\n * };\n * const response = interrupt(hitlRequest);\n * ```\n */\nexport interface HITLRequest {\n /**\n * A list of agent actions for human review.\n */\n actionRequests: ActionRequest[];\n /**\n * Review configuration for all possible actions.\n */\n reviewConfigs: ReviewConfig[];\n}\n\n/**\n * Response when a human approves the action.\n */\nexport interface ApproveDecision {\n type: \"approve\";\n}\n\n/**\n * Response when a human edits the action.\n */\nexport interface EditDecision {\n type: \"edit\";\n /**\n * Edited action for the agent to perform.\n * Ex: for a tool call, a human reviewer can edit the tool name and args.\n */\n editedAction: Action;\n}\n\n/**\n * Response when a human rejects the action.\n */\nexport interface RejectDecision {\n type: \"reject\";\n /**\n * The message sent to the model explaining why the action was rejected.\n */\n message?: string;\n}\n\n/**\n * Union of all possible decision types.\n */\nexport type Decision = ApproveDecision | EditDecision | RejectDecision;\n\n/**\n * Response payload for a HITLRequest.\n */\nexport interface HITLResponse {\n /**\n * The decisions made by the human.\n */\n decisions: Decision[];\n}\n\nconst contextSchema = z.object({\n /**\n * Mapping of tool name to allowed reviewer responses.\n * If a tool doesn't have an entry, it's auto-approved by default.\n *\n * - `true` -> pause for approval and allow approve/edit/reject decisions\n * - `false` -> auto-approve (no human review)\n * - `InterruptOnConfig` -> explicitly specify which decisions are allowed for this tool\n */\n interruptOn: z\n .record(z.union([z.boolean(), InterruptOnConfigSchema]))\n .optional(),\n /**\n * Prefix used when constructing human-facing approval messages.\n * Provides context about the tool call being reviewed; does not change the underlying action.\n *\n * Note: This prefix is only applied for tools that do not provide a custom\n * `description` via their {@link InterruptOnConfig}. If a tool specifies a custom\n * `description`, that per-tool text is used and this prefix is ignored.\n */\n descriptionPrefix: z.string().default(\"Tool execution requires approval\"),\n});\nexport type HumanInTheLoopMiddlewareConfig = InferInteropZodInput<\n typeof contextSchema\n>;\n\n/**\n * Creates a Human-in-the-Loop (HITL) middleware for tool approval and oversight.\n *\n * This middleware intercepts tool calls made by an AI agent and provides human oversight\n * capabilities before execution. It enables selective approval workflows where certain tools\n * require human intervention while others can execute automatically.\n *\n * A invocation result that has been interrupted by the middleware will have a `__interrupt__`\n * property that contains the interrupt request.\n *\n * ```ts\n * import { type HITLRequest, type HITLResponse } from \"langchain\";\n * import { type Interrupt } from \"langchain\";\n *\n * const result = await agent.invoke(request);\n * const interruptRequest = result.__interrupt__?.[0] as Interrupt<HITLRequest>;\n *\n * // Examine the action requests and review configs\n * const actionRequests = interruptRequest.value.actionRequests;\n * const reviewConfigs = interruptRequest.value.reviewConfigs;\n *\n * // Create decisions for each action\n * const resume: HITLResponse = {\n * decisions: actionRequests.map((action, i) => {\n * if (action.name === \"calculator\") {\n * return { type: \"approve\" };\n * } else if (action.name === \"write_file\") {\n * return {\n * type: \"edit\",\n * editedAction: { name: \"write_file\", args: { filename: \"safe.txt\", content: \"Safe content\" } }\n * };\n * }\n * return { type: \"reject\", message: \"Action not allowed\" };\n * })\n * };\n *\n * // Resume with decisions\n * await agent.invoke(new Command({ resume }), config);\n * ```\n *\n * ## Features\n *\n * - **Selective Tool Approval**: Configure which tools require human approval\n * - **Multiple Decision Types**: Approve, edit, or reject tool calls\n * - **Asynchronous Workflow**: Uses LangGraph's interrupt mechanism for non-blocking approval\n * - **Custom Approval Messages**: Provide context-specific descriptions for approval requests\n *\n * ## Decision Types\n *\n * When a tool requires approval, the human operator can respond with:\n * - `approve`: Execute the tool with original arguments\n * - `edit`: Modify the tool name and/or arguments before execution\n * - `reject`: Provide a manual response instead of executing the tool\n *\n * @param options - Configuration options for the middleware\n * @param options.interruptOn - Per-tool configuration mapping tool names to their settings\n * @param options.interruptOn[toolName].allowedDecisions - Array of decision types allowed for this tool (e.g., [\"approve\", \"edit\", \"reject\"])\n * @param options.interruptOn[toolName].description - Custom approval message for the tool. Can be either a static string or a callable that dynamically generates the description based on agent state, runtime, and tool call information\n * @param options.interruptOn[toolName].argsSchema - JSON schema for the arguments associated with the action, if edits are allowed\n * @param options.interruptOn[toolName].when - Optional predicate that dynamically controls whether a tool call triggers an interrupt. Returns `true` to interrupt or `false` to auto-approve the tool call.\n * @param options.descriptionPrefix - Default prefix for approval messages (default: \"Tool execution requires approval\"). Only used for tools that do not define a custom `description` in their InterruptOnConfig.\n *\n * @returns A middleware instance that can be passed to `createAgent`\n *\n * @example\n * Basic usage with selective tool approval\n * ```typescript\n * import { humanInTheLoopMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * const hitlMiddleware = humanInTheLoopMiddleware({\n * interruptOn: {\n * // Interrupt write_file tool and allow edits or approvals\n * \"write_file\": {\n * allowedDecisions: [\"approve\", \"edit\"],\n * description: \"⚠️ File write operation requires approval\"\n * },\n * // Auto-approve read_file tool\n * \"read_file\": false\n * }\n * });\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * tools: [writeFileTool, readFileTool],\n * middleware: [hitlMiddleware]\n * });\n * ```\n *\n * @example\n * Handling approval requests\n * ```typescript\n * import { type HITLRequest, type HITLResponse, type Interrupt } from \"langchain\";\n * import { Command } from \"@langchain/langgraph\";\n *\n * // Initial agent invocation\n * const result = await agent.invoke({\n * messages: [new HumanMessage(\"Write 'Hello' to output.txt\")]\n * }, config);\n *\n * // Check if agent is paused for approval\n * if (result.__interrupt__) {\n * const interruptRequest = result.__interrupt__?.[0] as Interrupt<HITLRequest>;\n *\n * // Show tool call details to user\n * console.log(\"Actions:\", interruptRequest.value.actionRequests);\n * console.log(\"Review configs:\", interruptRequest.value.reviewConfigs);\n *\n * // Resume with approval\n * const resume: HITLResponse = {\n * decisions: [{ type: \"approve\" }]\n * };\n * await agent.invoke(\n * new Command({ resume }),\n * config\n * );\n * }\n * ```\n *\n * @example\n * Different decision types\n * ```typescript\n * import { type HITLResponse } from \"langchain\";\n *\n * // Approve the tool call as-is\n * const resume: HITLResponse = {\n * decisions: [{ type: \"approve\" }]\n * };\n *\n * // Edit the tool arguments\n * const resume: HITLResponse = {\n * decisions: [{\n * type: \"edit\",\n * editedAction: { name: \"write_file\", args: { filename: \"safe.txt\", content: \"Modified\" } }\n * }]\n * };\n *\n * // Reject with feedback\n * const resume: HITLResponse = {\n * decisions: [{\n * type: \"reject\",\n * message: \"File operation not allowed in demo mode\"\n * }]\n * };\n * ```\n *\n * @example\n * Production use case with database operations\n * ```typescript\n * const hitlMiddleware = humanInTheLoopMiddleware({\n * interruptOn: {\n * \"execute_sql\": {\n * allowedDecisions: [\"approve\", \"edit\", \"reject\"],\n * description: \"🚨 SQL query requires DBA approval\\nPlease review for safety and performance\"\n * },\n * \"read_schema\": false, // Reading metadata is safe\n * \"delete_records\": {\n * allowedDecisions: [\"approve\", \"reject\"],\n * description: \"⛔ DESTRUCTIVE OPERATION - Requires manager approval\"\n * }\n * },\n * descriptionPrefix: \"Database operation pending approval\"\n * });\n * ```\n *\n * @example\n * Using dynamic callable descriptions\n * ```typescript\n * import { type DescriptionFactory, type ToolCall } from \"langchain\";\n * import type { AgentBuiltInState, Runtime } from \"langchain/agents\";\n *\n * // Define a dynamic description factory\n * const formatToolDescription: DescriptionFactory = (\n * toolCall: ToolCall,\n * state: AgentBuiltInState,\n * runtime: Runtime<unknown>\n * ) => {\n * return `Tool: ${toolCall.name}\\nArguments:\\n${JSON.stringify(toolCall.args, null, 2)}`;\n * };\n *\n * const hitlMiddleware = humanInTheLoopMiddleware({\n * interruptOn: {\n * \"write_file\": {\n * allowedDecisions: [\"approve\", \"edit\"],\n * // Use dynamic description that can access tool call, state, and runtime\n * description: formatToolDescription\n * },\n * // Or use an inline function\n * \"send_email\": {\n * allowedDecisions: [\"approve\", \"reject\"],\n * description: (toolCall, state, runtime) => {\n * const { to, subject } = toolCall.args;\n * return `Email to ${to}\\nSubject: ${subject}\\n\\nRequires approval before sending`;\n * }\n * }\n * }\n * });\n * ```\n *\n * @remarks\n * - Tool calls are processed in the order they appear in the AI message\n * - Auto-approved tools execute immediately without interruption\n * - Multiple tools requiring approval are bundled into a single interrupt request\n * - The middleware operates in the `afterModel` phase, intercepting before tool execution\n * - Requires a checkpointer to maintain state across interruptions\n *\n * @see {@link createAgent} for agent creation\n * @see {@link Command} for resuming interrupted execution\n * @public\n */\nexport function humanInTheLoopMiddleware(\n options: NonNullable<HumanInTheLoopMiddlewareConfig>\n) {\n const createActionAndConfig = async (\n toolCall: ToolCall,\n config: InterruptOnConfig,\n state: AgentBuiltInState,\n runtime: Runtime<unknown>\n ): Promise<{\n actionRequest: ActionRequest;\n reviewConfig: ReviewConfig;\n }> => {\n const toolName = toolCall.name;\n const toolArgs = toolCall.args;\n\n // Generate description using the description field (str or callable)\n const descriptionValue = config.description;\n let description: string;\n if (typeof descriptionValue === \"function\") {\n description = await descriptionValue(toolCall, state, runtime);\n } else if (descriptionValue !== undefined) {\n description = descriptionValue;\n } else {\n description = `${\n options.descriptionPrefix ?? \"Tool execution requires approval\"\n }\\n\\nTool: ${toolName}\\nArgs: ${JSON.stringify(toolArgs, null, 2)}`;\n }\n\n /**\n * Create ActionRequest with description\n */\n const actionRequest: ActionRequest = {\n name: toolName,\n args: toolArgs,\n description,\n };\n\n /**\n * Create ReviewConfig\n */\n const reviewConfig: ReviewConfig = {\n actionName: toolName,\n allowedDecisions: config.allowedDecisions,\n };\n\n if (config.argsSchema) {\n reviewConfig.argsSchema = config.argsSchema;\n }\n\n return { actionRequest, reviewConfig };\n };\n\n /**\n * Return `false` if the `when` predicate rejects this tool call, `true` otherwise.\n *\n * When no `when` predicate is configured the tool call always interrupts.\n */\n const shouldInterrupt = async (\n toolCall: ToolCall,\n config: InterruptOnConfig,\n state: AgentBuiltInState,\n runtime: Runtime<unknown>\n ): Promise<boolean> => {\n const { when } = config;\n if (when == null) {\n return true;\n }\n const request: ToolCallRequest<AgentBuiltInState> = {\n toolCall,\n tool: undefined,\n state,\n runtime,\n };\n return when(request);\n };\n\n const processDecision = (\n decision: Decision,\n toolCall: ToolCall,\n config: InterruptOnConfig\n ): { revisedToolCall: ToolCall | null; toolMessage: ToolMessage | null } => {\n const allowedDecisions = config.allowedDecisions;\n if (decision.type === \"approve\" && allowedDecisions.includes(\"approve\")) {\n return { revisedToolCall: toolCall, toolMessage: null };\n }\n\n if (decision.type === \"edit\" && allowedDecisions.includes(\"edit\")) {\n const editedAction = decision.editedAction;\n\n /**\n * Validate edited action structure\n */\n if (!editedAction || typeof editedAction.name !== \"string\") {\n throw new Error(\n `Invalid edited action for tool \"${toolCall.name}\": name must be a string`\n );\n }\n if (!editedAction.args || typeof editedAction.args !== \"object\") {\n throw new Error(\n `Invalid edited action for tool \"${toolCall.name}\": args must be an object`\n );\n }\n\n return {\n revisedToolCall: {\n type: \"tool_call\",\n name: editedAction.name,\n args: editedAction.args,\n id: toolCall.id,\n },\n toolMessage: null,\n };\n }\n\n if (decision.type === \"reject\" && allowedDecisions.includes(\"reject\")) {\n /**\n * Validate that message is a string if provided\n */\n if (\n decision.message !== undefined &&\n typeof decision.message !== \"string\"\n ) {\n throw new Error(\n `Tool call response for \"${\n toolCall.name\n }\" must be a string, got ${typeof decision.message}`\n );\n }\n\n // Create a tool message with the human's text response\n const content =\n decision.message ??\n `User rejected the tool call for \\`${toolCall.name}\\` with id ${toolCall.id}`;\n\n const toolMessage = new ToolMessage({\n content,\n name: toolCall.name,\n tool_call_id: toolCall.id!,\n status: \"error\",\n });\n\n return { revisedToolCall: toolCall, toolMessage };\n }\n\n const msg = `Unexpected human decision: ${JSON.stringify(\n decision\n )}. Decision type '${decision.type}' is not allowed for tool '${\n toolCall.name\n }'. Expected one of ${JSON.stringify(\n allowedDecisions\n )} based on the tool's configuration.`;\n throw new Error(msg);\n };\n\n return createMiddleware({\n name: \"HumanInTheLoopMiddleware\",\n contextSchema,\n afterModel: {\n canJumpTo: [\"model\"],\n hook: async (state, runtime) => {\n const config = interopParse(contextSchema, {\n ...options,\n ...(runtime.context || {}),\n });\n if (!config) {\n return;\n }\n\n const { messages } = state;\n if (!messages.length) {\n return;\n }\n\n /**\n * Don't do anything if the last message isn't an AI message with tool calls.\n */\n const lastMessage = [...messages]\n .reverse()\n .find((msg) => AIMessage.isInstance(msg)) as AIMessage;\n if (!lastMessage || !lastMessage.tool_calls?.length) {\n return;\n }\n\n /**\n * If the user omits the interruptOn config, we don't do anything.\n */\n if (!config.interruptOn) {\n return;\n }\n\n /**\n * Resolve per-tool configs (boolean true -> all decisions allowed; false -> auto-approve)\n */\n const resolvedConfigs: Record<string, InterruptOnConfig> = {};\n for (const [toolName, toolConfig] of Object.entries(\n config.interruptOn\n )) {\n if (typeof toolConfig === \"boolean\") {\n if (toolConfig === true) {\n resolvedConfigs[toolName] = {\n allowedDecisions: [...ALLOWED_DECISIONS],\n };\n }\n } else if (toolConfig.allowedDecisions) {\n resolvedConfigs[toolName] = toolConfig as InterruptOnConfig;\n }\n }\n\n const interruptToolCalls: ToolCall[] = [];\n const autoApprovedToolCalls: ToolCall[] = [];\n\n for (const toolCall of lastMessage.tool_calls) {\n const interruptConfig = resolvedConfigs[toolCall.name];\n /**\n * A tool call is interrupted only when it has a resolved config and its\n * optional `when` predicate doesn't opt it out. Otherwise it is\n * auto-approved.\n */\n if (\n interruptConfig &&\n (await shouldInterrupt(toolCall, interruptConfig, state, runtime))\n ) {\n interruptToolCalls.push(toolCall);\n } else {\n autoApprovedToolCalls.push(toolCall);\n }\n }\n\n /**\n * No interrupt tool calls, so we can just return.\n */\n if (!interruptToolCalls.length) {\n return;\n }\n\n /**\n * Create action requests and review configs for all tools that need approval\n */\n const actionRequests: ActionRequest[] = [];\n const reviewConfigs: ReviewConfig[] = [];\n\n for (const toolCall of interruptToolCalls) {\n const interruptConfig = resolvedConfigs[toolCall.name]!;\n\n /**\n * Create ActionRequest and ReviewConfig using helper method\n */\n const { actionRequest, reviewConfig } = await createActionAndConfig(\n toolCall,\n interruptConfig,\n state,\n runtime\n );\n actionRequests.push(actionRequest);\n reviewConfigs.push(reviewConfig);\n }\n\n /**\n * Create single HITLRequest with all actions and configs\n */\n const hitlRequest: HITLRequest = {\n actionRequests,\n reviewConfigs,\n };\n\n /**\n * Send interrupt and get response\n */\n const hitlResponse = (await interrupt(hitlRequest)) as HITLResponse;\n const decisions = hitlResponse.decisions;\n\n /**\n * Validate that decisions is a valid array before checking length\n */\n if (!decisions || !Array.isArray(decisions)) {\n throw new Error(\n \"Invalid HITLResponse: decisions must be a non-empty array\"\n );\n }\n\n /**\n * Validate that the number of decisions matches the number of interrupt tool calls\n */\n if (decisions.length !== interruptToolCalls.length) {\n throw new Error(\n `Number of human decisions (${decisions.length}) does not match number of hanging tool calls (${interruptToolCalls.length}).`\n );\n }\n\n const revisedToolCalls: ToolCall[] = [...autoApprovedToolCalls];\n const artificialToolMessages: ToolMessage[] = [];\n const hasRejectedToolCalls = decisions.some(\n (decision) => decision.type === \"reject\"\n );\n\n /**\n * Process each decision using helper method\n */\n for (let i = 0; i < decisions.length; i++) {\n const decision = decisions[i]!;\n const toolCall = interruptToolCalls[i]!;\n const interruptConfig = resolvedConfigs[toolCall.name]!;\n\n const { revisedToolCall, toolMessage } = processDecision(\n decision,\n toolCall,\n interruptConfig\n );\n\n if (\n revisedToolCall &&\n /**\n * If any decision is a rejected, we are going back to the model\n * with only the tool calls that were rejected as we don't know\n * the results of the approved/updated tool calls at this point.\n */\n (!hasRejectedToolCalls || decision.type === \"reject\")\n ) {\n revisedToolCalls.push(revisedToolCall);\n }\n if (toolMessage) {\n artificialToolMessages.push(toolMessage);\n }\n }\n\n /**\n * Update the AI message to only include approved tool calls\n */\n if (AIMessage.isInstance(lastMessage)) {\n lastMessage.tool_calls = revisedToolCalls;\n }\n\n const jumpTo: JumpToTarget | undefined = hasRejectedToolCalls\n ? \"model\"\n : undefined;\n return {\n messages: [lastMessage, ...artificialToolMessages],\n jumpTo,\n };\n },\n },\n });\n}\n"],"mappings":";;;;;;AAcA,MAAM,qBAAqB,EACxB,SAAS,CAAC,CACV,KAAK,EAAE,OAA2C,CAAC,CAAC,CACpD,QAAQ,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;AA2BzD,MAAM,4BAA4B,EAC/B,SAAS,CAAC,CACV,KACC,EAAE,OAAiB,GACnB,EAAE,OAA0B,GAC5B,EAAE,OAAyB,CAC7B,CAAC,CACA,QAAQ,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;;;;AAwBvD,MAAM,oBAAoB;CAAC;CAAW;CAAQ;AAAQ;AACtD,MAAM,eAAe,EAAE,KAAK,iBAAiB;AAG7C,MAAM,0BAA0B,EAAE,OAAO;;;;CAIvC,kBAAkB,EAAE,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CtC,aAAa,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,yBAAyB,CAAC,CAAC,CAAC,SAAS;;;;CAIvE,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS;;;;;;;;;;;;;;;;;;;;;;CAsBvC,MAAM,mBAAmB,SAAS;AACpC,CAAC;AAiID,MAAM,gBAAgB,EAAE,OAAO;;;;;;;;;CAS7B,aAAa,EACV,OAAO,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,uBAAuB,CAAC,CAAC,CAAC,CACvD,SAAS;;;;;;;;;CASZ,mBAAmB,EAAE,OAAO,CAAC,CAAC,QAAQ,kCAAkC;AAC1E,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuND,SAAgB,yBACd,SACA;CACA,MAAM,wBAAwB,OAC5B,UACA,QACA,OACA,YAII;EACJ,MAAM,WAAW,SAAS;EAC1B,MAAM,WAAW,SAAS;EAG1B,MAAM,mBAAmB,OAAO;EAChC,IAAI;EACJ,IAAI,OAAO,qBAAqB,YAC9B,cAAc,MAAM,iBAAiB,UAAU,OAAO,OAAO;OACxD,IAAI,qBAAqB,KAAA,GAC9B,cAAc;OAEd,cAAc,GACZ,QAAQ,qBAAqB,mCAC9B,YAAY,SAAS,UAAU,KAAK,UAAU,UAAU,MAAM,CAAC;;;;EAMlE,MAAM,gBAA+B;GACnC,MAAM;GACN,MAAM;GACN;EACF;;;;EAKA,MAAM,eAA6B;GACjC,YAAY;GACZ,kBAAkB,OAAO;EAC3B;EAEA,IAAI,OAAO,YACT,aAAa,aAAa,OAAO;EAGnC,OAAO;GAAE;GAAe;EAAa;CACvC;;;;;;CAOA,MAAM,kBAAkB,OACtB,UACA,QACA,OACA,YACqB;EACrB,MAAM,EAAE,SAAS;EACjB,IAAI,QAAQ,MACV,OAAO;EAQT,OAAO,KAAK;GALV;GACA,MAAM,KAAA;GACN;GACA;EAEgB,CAAC;CACrB;CAEA,MAAM,mBACJ,UACA,UACA,WAC0E;EAC1E,MAAM,mBAAmB,OAAO;EAChC,IAAI,SAAS,SAAS,aAAa,iBAAiB,SAAS,SAAS,GACpE,OAAO;GAAE,iBAAiB;GAAU,aAAa;EAAK;EAGxD,IAAI,SAAS,SAAS,UAAU,iBAAiB,SAAS,MAAM,GAAG;GACjE,MAAM,eAAe,SAAS;;;;GAK9B,IAAI,CAAC,gBAAgB,OAAO,aAAa,SAAS,UAChD,MAAM,IAAI,MACR,mCAAmC,SAAS,KAAK,yBACnD;GAEF,IAAI,CAAC,aAAa,QAAQ,OAAO,aAAa,SAAS,UACrD,MAAM,IAAI,MACR,mCAAmC,SAAS,KAAK,0BACnD;GAGF,OAAO;IACL,iBAAiB;KACf,MAAM;KACN,MAAM,aAAa;KACnB,MAAM,aAAa;KACnB,IAAI,SAAS;IACf;IACA,aAAa;GACf;EACF;EAEA,IAAI,SAAS,SAAS,YAAY,iBAAiB,SAAS,QAAQ,GAAG;;;;GAIrE,IACE,SAAS,YAAY,KAAA,KACrB,OAAO,SAAS,YAAY,UAE5B,MAAM,IAAI,MACR,2BACE,SAAS,KACV,0BAA0B,OAAO,SAAS,SAC7C;GAeF,OAAO;IAAE,iBAAiB;IAAU,aAAA,IAPZ,YAAY;KAClC,SAJA,SAAS,WACT,qCAAqC,SAAS,KAAK,aAAa,SAAS;KAIzE,MAAM,SAAS;KACf,cAAc,SAAS;KACvB,QAAQ;IACV,CAE8C;GAAE;EAClD;EAEA,MAAM,MAAM,8BAA8B,KAAK,UAC7C,QACF,EAAE,mBAAmB,SAAS,KAAK,6BACjC,SAAS,KACV,qBAAqB,KAAK,UACzB,gBACF,EAAE;EACF,MAAM,IAAI,MAAM,GAAG;CACrB;CAEA,OAAO,iBAAiB;EACtB,MAAM;EACN;EACA,YAAY;GACV,WAAW,CAAC,OAAO;GACnB,MAAM,OAAO,OAAO,YAAY;IAC9B,MAAM,SAAS,aAAa,eAAe;KACzC,GAAG;KACH,GAAI,QAAQ,WAAW,CAAC;IAC1B,CAAC;IACD,IAAI,CAAC,QACH;IAGF,MAAM,EAAE,aAAa;IACrB,IAAI,CAAC,SAAS,QACZ;;;;IAMF,MAAM,cAAc,CAAC,GAAG,QAAQ,CAAC,CAC9B,QAAQ,CAAC,CACT,MAAM,QAAQ,UAAU,WAAW,GAAG,CAAC;IAC1C,IAAI,CAAC,eAAe,CAAC,YAAY,YAAY,QAC3C;;;;IAMF,IAAI,CAAC,OAAO,aACV;;;;IAMF,MAAM,kBAAqD,CAAC;IAC5D,KAAK,MAAM,CAAC,UAAU,eAAe,OAAO,QAC1C,OAAO,WACT,GACE,IAAI,OAAO,eAAe,WACpB;SAAA,eAAe,MACjB,gBAAgB,YAAY,EAC1B,kBAAkB,CAAC,GAAG,iBAAiB,EACzC;IAAA,OAEG,IAAI,WAAW,kBACpB,gBAAgB,YAAY;IAIhC,MAAM,qBAAiC,CAAC;IACxC,MAAM,wBAAoC,CAAC;IAE3C,KAAK,MAAM,YAAY,YAAY,YAAY;KAC7C,MAAM,kBAAkB,gBAAgB,SAAS;;;;;;KAMjD,IACE,mBACC,MAAM,gBAAgB,UAAU,iBAAiB,OAAO,OAAO,GAEhE,mBAAmB,KAAK,QAAQ;UAEhC,sBAAsB,KAAK,QAAQ;IAEvC;;;;IAKA,IAAI,CAAC,mBAAmB,QACtB;;;;IAMF,MAAM,iBAAkC,CAAC;IACzC,MAAM,gBAAgC,CAAC;IAEvC,KAAK,MAAM,YAAY,oBAAoB;KACzC,MAAM,kBAAkB,gBAAgB,SAAS;;;;KAKjD,MAAM,EAAE,eAAe,iBAAiB,MAAM,sBAC5C,UACA,iBACA,OACA,OACF;KACA,eAAe,KAAK,aAAa;KACjC,cAAc,KAAK,YAAY;IACjC;IAcA,MAAM,aAAY,MADU,UAAU;KAPpC;KACA;IAM8C,CAAC,EAAA,CAClB;;;;IAK/B,IAAI,CAAC,aAAa,CAAC,MAAM,QAAQ,SAAS,GACxC,MAAM,IAAI,MACR,2DACF;;;;IAMF,IAAI,UAAU,WAAW,mBAAmB,QAC1C,MAAM,IAAI,MACR,8BAA8B,UAAU,OAAO,iDAAiD,mBAAmB,OAAO,GAC5H;IAGF,MAAM,mBAA+B,CAAC,GAAG,qBAAqB;IAC9D,MAAM,yBAAwC,CAAC;IAC/C,MAAM,uBAAuB,UAAU,MACpC,aAAa,SAAS,SAAS,QAClC;;;;IAKA,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;KACzC,MAAM,WAAW,UAAU;KAC3B,MAAM,WAAW,mBAAmB;KACpC,MAAM,kBAAkB,gBAAgB,SAAS;KAEjD,MAAM,EAAE,iBAAiB,gBAAgB,gBACvC,UACA,UACA,eACF;KAEA,IACE,oBAMC,CAAC,wBAAwB,SAAS,SAAS,WAE5C,iBAAiB,KAAK,eAAe;KAEvC,IAAI,aACF,uBAAuB,KAAK,WAAW;IAE3C;;;;IAKA,IAAI,UAAU,WAAW,WAAW,GAClC,YAAY,aAAa;IAG3B,MAAM,SAAmC,uBACrC,UACA,KAAA;IACJ,OAAO;KACL,UAAU,CAAC,aAAa,GAAG,sBAAsB;KACjD;IACF;GACF;EACF;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"hitl.js","names":[],"sources":["../../../src/agents/middleware/hitl.ts"],"sourcesContent":["/* oxlint-disable @typescript-eslint/no-explicit-any */\nimport { z } from \"zod/v3\";\nimport { AIMessage, ToolMessage, ToolCall } from \"@langchain/core/messages\";\nimport {\n InferInteropZodInput,\n interopParse,\n} from \"@langchain/core/utils/types\";\nimport { interrupt } from \"@langchain/langgraph\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport type { AgentBuiltInState, Runtime } from \"../runtime.js\";\nimport type { JumpToTarget } from \"../constants.js\";\nimport type { ToolCallRequest } from \"./types.js\";\n\nconst WhenFunctionSchema = z\n .function()\n .args(z.custom<ToolCallRequest<AgentBuiltInState>>()) // request\n .returns(z.union([z.boolean(), z.promise(z.boolean())]));\n\n/**\n * Predicate controlling whether a given tool call triggers an interrupt.\n *\n * Receives a {@link ToolCallRequest} and returns `true` to interrupt or `false`\n * to auto-approve the tool call.\n *\n * The request is constructed with `tool` set to `undefined` and `runtime` set to\n * the node-level {@link Runtime}, so it reflects the batch (`afterModel`) context\n * rather than a per-call tool execution.\n *\n * @param request - The tool call request being evaluated\n * @returns `true` to interrupt for the tool call, `false` to auto-approve it.\n * May also return a promise resolving to a boolean.\n *\n * @example\n * ```typescript\n * import { type WhenPredicate } from \"langchain\";\n *\n * // Only interrupt delete_file calls targeting /etc\n * const when: WhenPredicate = (request) =>\n * String(request.toolCall.args.path ?? \"\").startsWith(\"/etc\");\n * ```\n */\nexport type WhenPredicate = z.infer<typeof WhenFunctionSchema>;\n\nconst DescriptionFunctionSchema = z\n .function()\n .args(\n z.custom<ToolCall>(), // toolCall\n z.custom<AgentBuiltInState>(), // state\n z.custom<Runtime<unknown>>() // runtime\n )\n .returns(z.union([z.string(), z.promise(z.string())]));\n\n/**\n * Function type that dynamically generates a description for a tool call approval request.\n *\n * @param toolCall - The tool call being reviewed\n * @param state - The current agent state\n * @param runtime - The agent runtime context\n * @returns A string description or Promise that resolves to a string description\n *\n * @example\n * ```typescript\n * import { type DescriptionFactory, type ToolCall } from \"langchain\";\n *\n * const descriptionFactory: DescriptionFactory = (toolCall, state, runtime) => {\n * return `Please review: ${toolCall.name}(${JSON.stringify(toolCall.args)})`;\n * };\n * ```\n */\nexport type DescriptionFactory = z.infer<typeof DescriptionFunctionSchema>;\n\n/**\n * The type of decision a human can make.\n */\nconst ALLOWED_DECISIONS = [\"approve\", \"edit\", \"reject\"] as const;\nconst DecisionType = z.enum(ALLOWED_DECISIONS);\nexport type DecisionType = z.infer<typeof DecisionType>;\n\nconst InterruptOnConfigSchema = z.object({\n /**\n * The decisions that are allowed for this action.\n */\n allowedDecisions: z.array(DecisionType),\n /**\n * The description attached to the request for human input.\n * Can be either:\n * - A static string describing the approval request\n * - A callable that dynamically generates the description based on agent state,\n * runtime, and tool call information\n *\n * @example\n * Static string description\n * ```typescript\n * import type { InterruptOnConfig } from \"langchain\";\n *\n * const config: InterruptOnConfig = {\n * allowedDecisions: [\"approve\", \"reject\"],\n * description: \"Please review this tool execution\"\n * };\n * ```\n *\n * @example\n * Dynamic callable description\n * ```typescript\n * import type {\n * AgentBuiltInState,\n * Runtime,\n * DescriptionFactory,\n * ToolCall,\n * InterruptOnConfig\n * } from \"langchain\";\n *\n * const formatToolDescription: DescriptionFactory = (\n * toolCall: ToolCall,\n * state: AgentBuiltInState,\n * runtime: Runtime<unknown>\n * ) => {\n * return `Tool: ${toolCall.name}\\nArguments:\\n${JSON.stringify(toolCall.args, null, 2)}`;\n * };\n *\n * const config: InterruptOnConfig = {\n * allowedDecisions: [\"approve\", \"edit\"],\n * description: formatToolDescription\n * };\n * ```\n */\n description: z.union([z.string(), DescriptionFunctionSchema]).optional(),\n /**\n * JSON schema for the arguments associated with the action, if edits are allowed.\n */\n argsSchema: z.record(z.any()).optional(),\n /**\n * Optional predicate controlling whether to interrupt for a given tool call.\n *\n * Receives a {@link ToolCallRequest} and returns `true` to interrupt or\n * `false` to auto-approve the tool call.\n *\n * The request is constructed with `tool` set to `undefined` and `runtime` set\n * to the node-level {@link Runtime}, so `request.tool` is not available.\n *\n * @example\n * ```typescript\n * import type { InterruptOnConfig } from \"langchain\";\n *\n * // Only interrupt delete_file calls targeting /etc\n * const config: InterruptOnConfig = {\n * allowedDecisions: [\"approve\", \"reject\"],\n * when: (request) =>\n * String(request.toolCall.args.path ?? \"\").startsWith(\"/etc\"),\n * };\n * ```\n */\n when: WhenFunctionSchema.optional(),\n});\nexport type InterruptOnConfig = z.input<typeof InterruptOnConfigSchema>;\n\n/**\n * Represents an action with a name and arguments.\n */\nexport interface Action {\n /**\n * The type or name of action being requested (e.g., \"add_numbers\").\n */\n name: string;\n /**\n * Key-value pairs of arguments needed for the action (e.g., {\"a\": 1, \"b\": 2}).\n */\n args: Record<string, any>;\n}\n\n/**\n * Represents an action request with a name, arguments, and description.\n */\nexport interface ActionRequest {\n /**\n * The name of the action being requested.\n */\n name: string;\n /**\n * Key-value pairs of arguments needed for the action (e.g., {\"a\": 1, \"b\": 2}).\n */\n args: Record<string, any>;\n /**\n * The description of the action to be reviewed.\n */\n description?: string;\n}\n\n/**\n * Policy for reviewing a HITL request.\n */\nexport interface ReviewConfig {\n /**\n * Name of the action associated with this review configuration.\n */\n actionName: string;\n /**\n * The decisions that are allowed for this request.\n */\n allowedDecisions: DecisionType[];\n /**\n * JSON schema for the arguments associated with the action, if edits are allowed.\n */\n argsSchema?: Record<string, any>;\n}\n\n/**\n * Request for human feedback on a sequence of actions requested by a model.\n *\n * @example\n * ```ts\n * const hitlRequest: HITLRequest = {\n * actionRequests: [\n * { name: \"send_email\", args: { to: \"user@example.com\", subject: \"Hello\" } }\n * ],\n * reviewConfigs: [\n * {\n * actionName: \"send_email\",\n * allowedDecisions: [\"approve\", \"edit\", \"reject\"],\n * description: \"Please review the email before sending\"\n * }\n * ]\n * };\n * const response = interrupt(hitlRequest);\n * ```\n */\nexport interface HITLRequest {\n /**\n * A list of agent actions for human review.\n */\n actionRequests: ActionRequest[];\n /**\n * Review configuration for all possible actions.\n */\n reviewConfigs: ReviewConfig[];\n}\n\n/**\n * Response when a human approves the action.\n */\nexport interface ApproveDecision {\n type: \"approve\";\n}\n\n/**\n * Response when a human edits the action.\n */\nexport interface EditDecision {\n type: \"edit\";\n /**\n * Edited action for the agent to perform.\n * Ex: for a tool call, a human reviewer can edit the tool name and args.\n */\n editedAction: Action;\n}\n\n/**\n * Response when a human rejects the action.\n */\nexport interface RejectDecision {\n type: \"reject\";\n /**\n * The message sent to the model explaining why the action was rejected.\n */\n message?: string;\n}\n\n/**\n * Union of all possible decision types.\n */\nexport type Decision = ApproveDecision | EditDecision | RejectDecision;\n\n/**\n * Response payload for a HITLRequest.\n */\nexport interface HITLResponse {\n /**\n * The decisions made by the human.\n */\n decisions: Decision[];\n}\n\nconst contextSchema = z.object({\n /**\n * Mapping of tool name to allowed reviewer responses.\n * If a tool doesn't have an entry, it's auto-approved by default.\n *\n * - `true` -> pause for approval and allow approve/edit/reject decisions\n * - `false` -> auto-approve (no human review)\n * - `InterruptOnConfig` -> explicitly specify which decisions are allowed for this tool\n */\n interruptOn: z\n .record(z.union([z.boolean(), InterruptOnConfigSchema]))\n .optional(),\n /**\n * Prefix used when constructing human-facing approval messages.\n * Provides context about the tool call being reviewed; does not change the underlying action.\n *\n * Note: This prefix is only applied for tools that do not provide a custom\n * `description` via their {@link InterruptOnConfig}. If a tool specifies a custom\n * `description`, that per-tool text is used and this prefix is ignored.\n */\n descriptionPrefix: z.string().default(\"Tool execution requires approval\"),\n});\nexport type HumanInTheLoopMiddlewareConfig = InferInteropZodInput<\n typeof contextSchema\n>;\n\n/**\n * Creates a Human-in-the-Loop (HITL) middleware for tool approval and oversight.\n *\n * This middleware intercepts tool calls made by an AI agent and provides human oversight\n * capabilities before execution. It enables selective approval workflows where certain tools\n * require human intervention while others can execute automatically.\n *\n * A invocation result that has been interrupted by the middleware will have a `__interrupt__`\n * property that contains the interrupt request.\n *\n * ```ts\n * import { type HITLRequest, type HITLResponse } from \"langchain\";\n * import { type Interrupt } from \"langchain\";\n *\n * const result = await agent.invoke(request);\n * const interruptRequest = result.__interrupt__?.[0] as Interrupt<HITLRequest>;\n *\n * // Examine the action requests and review configs\n * const actionRequests = interruptRequest.value.actionRequests;\n * const reviewConfigs = interruptRequest.value.reviewConfigs;\n *\n * // Create decisions for each action\n * const resume: HITLResponse = {\n * decisions: actionRequests.map((action, i) => {\n * if (action.name === \"calculator\") {\n * return { type: \"approve\" };\n * } else if (action.name === \"write_file\") {\n * return {\n * type: \"edit\",\n * editedAction: { name: \"write_file\", args: { filename: \"safe.txt\", content: \"Safe content\" } }\n * };\n * }\n * return { type: \"reject\", message: \"Action not allowed\" };\n * })\n * };\n *\n * // Resume with decisions\n * await agent.invoke(new Command({ resume }), config);\n * ```\n *\n * ## Features\n *\n * - **Selective Tool Approval**: Configure which tools require human approval\n * - **Multiple Decision Types**: Approve, edit, or reject tool calls\n * - **Asynchronous Workflow**: Uses LangGraph's interrupt mechanism for non-blocking approval\n * - **Custom Approval Messages**: Provide context-specific descriptions for approval requests\n *\n * ## Decision Types\n *\n * When a tool requires approval, the human operator can respond with:\n * - `approve`: Execute the tool with original arguments\n * - `edit`: Modify the tool name and/or arguments before execution\n * - `reject`: Provide a manual response instead of executing the tool\n *\n * @param options - Configuration options for the middleware\n * @param options.interruptOn - Per-tool configuration mapping tool names to their settings\n * @param options.interruptOn[toolName].allowedDecisions - Array of decision types allowed for this tool (e.g., [\"approve\", \"edit\", \"reject\"])\n * @param options.interruptOn[toolName].description - Custom approval message for the tool. Can be either a static string or a callable that dynamically generates the description based on agent state, runtime, and tool call information\n * @param options.interruptOn[toolName].argsSchema - JSON schema for the arguments associated with the action, if edits are allowed\n * @param options.interruptOn[toolName].when - Optional predicate that dynamically controls whether a tool call triggers an interrupt. Returns `true` to interrupt or `false` to auto-approve the tool call.\n * @param options.descriptionPrefix - Default prefix for approval messages (default: \"Tool execution requires approval\"). Only used for tools that do not define a custom `description` in their InterruptOnConfig.\n *\n * @returns A middleware instance that can be passed to `createAgent`\n *\n * @example\n * Basic usage with selective tool approval\n * ```typescript\n * import { humanInTheLoopMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * const hitlMiddleware = humanInTheLoopMiddleware({\n * interruptOn: {\n * // Interrupt write_file tool and allow edits or approvals\n * \"write_file\": {\n * allowedDecisions: [\"approve\", \"edit\"],\n * description: \"⚠️ File write operation requires approval\"\n * },\n * // Auto-approve read_file tool\n * \"read_file\": false\n * }\n * });\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * tools: [writeFileTool, readFileTool],\n * middleware: [hitlMiddleware]\n * });\n * ```\n *\n * @example\n * Handling approval requests\n * ```typescript\n * import { type HITLRequest, type HITLResponse, type Interrupt } from \"langchain\";\n * import { Command } from \"@langchain/langgraph\";\n *\n * // Initial agent invocation\n * const result = await agent.invoke({\n * messages: [new HumanMessage(\"Write 'Hello' to output.txt\")]\n * }, config);\n *\n * // Check if agent is paused for approval\n * if (result.__interrupt__) {\n * const interruptRequest = result.__interrupt__?.[0] as Interrupt<HITLRequest>;\n *\n * // Show tool call details to user\n * console.log(\"Actions:\", interruptRequest.value.actionRequests);\n * console.log(\"Review configs:\", interruptRequest.value.reviewConfigs);\n *\n * // Resume with approval\n * const resume: HITLResponse = {\n * decisions: [{ type: \"approve\" }]\n * };\n * await agent.invoke(\n * new Command({ resume }),\n * config\n * );\n * }\n * ```\n *\n * @example\n * Different decision types\n * ```typescript\n * import { type HITLResponse } from \"langchain\";\n *\n * // Approve the tool call as-is\n * const resume: HITLResponse = {\n * decisions: [{ type: \"approve\" }]\n * };\n *\n * // Edit the tool arguments\n * const resume: HITLResponse = {\n * decisions: [{\n * type: \"edit\",\n * editedAction: { name: \"write_file\", args: { filename: \"safe.txt\", content: \"Modified\" } }\n * }]\n * };\n *\n * // Reject with feedback\n * const resume: HITLResponse = {\n * decisions: [{\n * type: \"reject\",\n * message: \"File operation not allowed in demo mode\"\n * }]\n * };\n * ```\n *\n * @example\n * Production use case with database operations\n * ```typescript\n * const hitlMiddleware = humanInTheLoopMiddleware({\n * interruptOn: {\n * \"execute_sql\": {\n * allowedDecisions: [\"approve\", \"edit\", \"reject\"],\n * description: \"🚨 SQL query requires DBA approval\\nPlease review for safety and performance\"\n * },\n * \"read_schema\": false, // Reading metadata is safe\n * \"delete_records\": {\n * allowedDecisions: [\"approve\", \"reject\"],\n * description: \"⛔ DESTRUCTIVE OPERATION - Requires manager approval\"\n * }\n * },\n * descriptionPrefix: \"Database operation pending approval\"\n * });\n * ```\n *\n * @example\n * Using dynamic callable descriptions\n * ```typescript\n * import { type DescriptionFactory, type ToolCall } from \"langchain\";\n * import type { AgentBuiltInState, Runtime } from \"langchain/agents\";\n *\n * // Define a dynamic description factory\n * const formatToolDescription: DescriptionFactory = (\n * toolCall: ToolCall,\n * state: AgentBuiltInState,\n * runtime: Runtime<unknown>\n * ) => {\n * return `Tool: ${toolCall.name}\\nArguments:\\n${JSON.stringify(toolCall.args, null, 2)}`;\n * };\n *\n * const hitlMiddleware = humanInTheLoopMiddleware({\n * interruptOn: {\n * \"write_file\": {\n * allowedDecisions: [\"approve\", \"edit\"],\n * // Use dynamic description that can access tool call, state, and runtime\n * description: formatToolDescription\n * },\n * // Or use an inline function\n * \"send_email\": {\n * allowedDecisions: [\"approve\", \"reject\"],\n * description: (toolCall, state, runtime) => {\n * const { to, subject } = toolCall.args;\n * return `Email to ${to}\\nSubject: ${subject}\\n\\nRequires approval before sending`;\n * }\n * }\n * }\n * });\n * ```\n *\n * @remarks\n * - Tool calls are processed in the order they appear in the AI message\n * - Auto-approved tools execute immediately without interruption\n * - Multiple tools requiring approval are bundled into a single interrupt request\n * - The middleware operates in the `afterModel` phase, intercepting before tool execution\n * - Requires a checkpointer to maintain state across interruptions\n *\n * @see {@link createAgent} for agent creation\n * @see {@link Command} for resuming interrupted execution\n * @public\n */\nexport function humanInTheLoopMiddleware(\n options: NonNullable<HumanInTheLoopMiddlewareConfig>\n) {\n const createActionAndConfig = async (\n toolCall: ToolCall,\n config: InterruptOnConfig,\n state: AgentBuiltInState,\n runtime: Runtime<unknown>\n ): Promise<{\n actionRequest: ActionRequest;\n reviewConfig: ReviewConfig;\n }> => {\n const toolName = toolCall.name;\n const toolArgs = toolCall.args;\n\n // Generate description using the description field (str or callable)\n const descriptionValue = config.description;\n let description: string;\n if (typeof descriptionValue === \"function\") {\n description = await descriptionValue(toolCall, state, runtime);\n } else if (descriptionValue !== undefined) {\n description = descriptionValue;\n } else {\n description = `${\n options.descriptionPrefix ?? \"Tool execution requires approval\"\n }\\n\\nTool: ${toolName}\\nArgs: ${JSON.stringify(toolArgs, null, 2)}`;\n }\n\n /**\n * Create ActionRequest with description\n */\n const actionRequest: ActionRequest = {\n name: toolName,\n args: toolArgs,\n description,\n };\n\n /**\n * Create ReviewConfig\n */\n const reviewConfig: ReviewConfig = {\n actionName: toolName,\n allowedDecisions: config.allowedDecisions,\n };\n\n if (config.argsSchema) {\n reviewConfig.argsSchema = config.argsSchema;\n }\n\n return { actionRequest, reviewConfig };\n };\n\n /**\n * Return `false` if the `when` predicate rejects this tool call, `true` otherwise.\n *\n * When no `when` predicate is configured the tool call always interrupts.\n */\n const shouldInterrupt = async (\n toolCall: ToolCall,\n config: InterruptOnConfig,\n state: AgentBuiltInState,\n runtime: Runtime<unknown>\n ): Promise<boolean> => {\n const { when } = config;\n if (when == null) {\n return true;\n }\n const request: ToolCallRequest<AgentBuiltInState> = {\n toolCall,\n tool: undefined,\n state,\n runtime,\n };\n return when(request);\n };\n\n const processDecision = (\n decision: Decision,\n toolCall: ToolCall,\n config: InterruptOnConfig\n ): { revisedToolCall: ToolCall | null; toolMessage: ToolMessage | null } => {\n const allowedDecisions = config.allowedDecisions;\n if (decision.type === \"approve\" && allowedDecisions.includes(\"approve\")) {\n return { revisedToolCall: toolCall, toolMessage: null };\n }\n\n if (decision.type === \"edit\" && allowedDecisions.includes(\"edit\")) {\n const editedAction = decision.editedAction;\n\n /**\n * Validate edited action structure\n */\n if (!editedAction || typeof editedAction.name !== \"string\") {\n throw new Error(\n `Invalid edited action for tool \"${toolCall.name}\": name must be a string`\n );\n }\n if (!editedAction.args || typeof editedAction.args !== \"object\") {\n throw new Error(\n `Invalid edited action for tool \"${toolCall.name}\": args must be an object`\n );\n }\n\n return {\n revisedToolCall: {\n type: \"tool_call\",\n name: editedAction.name,\n args: editedAction.args,\n id: toolCall.id,\n },\n toolMessage: null,\n };\n }\n\n if (decision.type === \"reject\" && allowedDecisions.includes(\"reject\")) {\n /**\n * Validate that message is a string if provided\n */\n if (\n decision.message !== undefined &&\n typeof decision.message !== \"string\"\n ) {\n throw new Error(\n `Tool call response for \"${\n toolCall.name\n }\" must be a string, got ${typeof decision.message}`\n );\n }\n\n // Create a tool message with the human's text response\n const content =\n decision.message ??\n `User rejected the tool call for \\`${toolCall.name}\\` with id ${toolCall.id}`;\n\n const toolMessage = new ToolMessage({\n content,\n name: toolCall.name,\n tool_call_id: toolCall.id!,\n status: \"error\",\n });\n\n return { revisedToolCall: toolCall, toolMessage };\n }\n\n const msg = `Unexpected human decision: ${JSON.stringify(\n decision\n )}. Decision type '${decision.type}' is not allowed for tool '${\n toolCall.name\n }'. Expected one of ${JSON.stringify(\n allowedDecisions\n )} based on the tool's configuration.`;\n throw new Error(msg);\n };\n\n return createMiddleware({\n name: \"HumanInTheLoopMiddleware\",\n contextSchema,\n afterModel: {\n canJumpTo: [\"model\"],\n hook: async (state, runtime) => {\n const config = interopParse(contextSchema, {\n ...options,\n ...(runtime.context || {}),\n });\n if (!config) {\n return;\n }\n\n const { messages } = state;\n if (!messages.length) {\n return;\n }\n\n /**\n * Don't do anything if the last message isn't an AI message with tool calls.\n */\n const lastMessage = [...messages]\n .reverse()\n .find((msg) => AIMessage.isInstance(msg)) as AIMessage;\n if (!lastMessage || !lastMessage.tool_calls?.length) {\n return;\n }\n\n /**\n * If the user omits the interruptOn config, we don't do anything.\n */\n if (!config.interruptOn) {\n return;\n }\n\n /**\n * Resolve per-tool configs (boolean true -> all decisions allowed; false -> auto-approve)\n */\n const resolvedConfigs: Record<string, InterruptOnConfig> = {};\n for (const [toolName, toolConfig] of Object.entries(\n config.interruptOn\n )) {\n if (typeof toolConfig === \"boolean\") {\n if (toolConfig === true) {\n resolvedConfigs[toolName] = {\n allowedDecisions: [...ALLOWED_DECISIONS],\n };\n }\n } else if (toolConfig.allowedDecisions) {\n resolvedConfigs[toolName] = toolConfig as InterruptOnConfig;\n }\n }\n\n const interruptToolCalls: ToolCall[] = [];\n const autoApprovedToolCalls: ToolCall[] = [];\n\n for (const toolCall of lastMessage.tool_calls) {\n const interruptConfig = resolvedConfigs[toolCall.name];\n /**\n * A tool call is interrupted only when it has a resolved config and its\n * optional `when` predicate doesn't opt it out. Otherwise it is\n * auto-approved.\n */\n if (\n interruptConfig &&\n (await shouldInterrupt(toolCall, interruptConfig, state, runtime))\n ) {\n interruptToolCalls.push(toolCall);\n } else {\n autoApprovedToolCalls.push(toolCall);\n }\n }\n\n /**\n * No interrupt tool calls, so we can just return.\n */\n if (!interruptToolCalls.length) {\n return;\n }\n\n /**\n * Create action requests and review configs for all tools that need approval\n */\n const actionRequests: ActionRequest[] = [];\n const reviewConfigs: ReviewConfig[] = [];\n\n for (const toolCall of interruptToolCalls) {\n const interruptConfig = resolvedConfigs[toolCall.name]!;\n\n /**\n * Create ActionRequest and ReviewConfig using helper method\n */\n const { actionRequest, reviewConfig } = await createActionAndConfig(\n toolCall,\n interruptConfig,\n state,\n runtime\n );\n actionRequests.push(actionRequest);\n reviewConfigs.push(reviewConfig);\n }\n\n /**\n * Create single HITLRequest with all actions and configs\n */\n const hitlRequest: HITLRequest = {\n actionRequests,\n reviewConfigs,\n };\n\n /**\n * Send interrupt and get response\n */\n const hitlResponse = (await interrupt(hitlRequest)) as HITLResponse;\n const decisions = hitlResponse.decisions;\n\n /**\n * Validate that decisions is a valid array before checking length\n */\n if (!decisions || !Array.isArray(decisions)) {\n throw new Error(\n \"Invalid HITLResponse: decisions must be a non-empty array\"\n );\n }\n\n /**\n * Validate that the number of decisions matches the number of interrupt tool calls\n */\n if (decisions.length !== interruptToolCalls.length) {\n throw new Error(\n `Number of human decisions (${decisions.length}) does not match number of hanging tool calls (${interruptToolCalls.length}).`\n );\n }\n\n const revisedToolCalls: ToolCall[] = [...autoApprovedToolCalls];\n const artificialToolMessages: ToolMessage[] = [];\n const hasRejectedToolCalls = decisions.some(\n (decision) => decision.type === \"reject\"\n );\n\n /**\n * Process each decision using helper method\n */\n for (let i = 0; i < decisions.length; i++) {\n const decision = decisions[i]!;\n const toolCall = interruptToolCalls[i]!;\n const interruptConfig = resolvedConfigs[toolCall.name]!;\n\n const { revisedToolCall, toolMessage } = processDecision(\n decision,\n toolCall,\n interruptConfig\n );\n\n if (\n revisedToolCall &&\n /**\n * If any decision is a rejected, we are going back to the model\n * with only the tool calls that were rejected as we don't know\n * the results of the approved/updated tool calls at this point.\n */\n (!hasRejectedToolCalls || decision.type === \"reject\")\n ) {\n revisedToolCalls.push(revisedToolCall);\n }\n if (toolMessage) {\n artificialToolMessages.push(toolMessage);\n }\n }\n\n /**\n * Update the AI message to only include approved tool calls\n */\n if (AIMessage.isInstance(lastMessage)) {\n lastMessage.tool_calls = revisedToolCalls;\n }\n\n const jumpTo: JumpToTarget | undefined = hasRejectedToolCalls\n ? \"model\"\n : undefined;\n return {\n messages: [lastMessage, ...artificialToolMessages],\n jumpTo,\n };\n },\n },\n });\n}\n"],"mappings":"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"}
@@ -89,7 +89,8 @@ function llmToolSelectorMiddleware(options) {
89
89
  if (!selectionRequest) return handler(request);
90
90
  const toolSelectionSchema = createToolSelectionResponse(selectionRequest.availableTools);
91
91
  const structuredModel = await selectionRequest.model.withStructuredOutput?.(toolSelectionSchema);
92
- const config = (0, _langchain_core_runnables.mergeConfigs)((0, _langchain_core_runnables.pickRunnableConfigKeys)(request.runtime) ?? {}, {
92
+ const baseConfig = (0, _langchain_core_runnables.pickRunnableConfigKeys)(request.runtime) ?? {};
93
+ const config = (0, _langchain_core_runnables.mergeConfigs)(baseConfig, {
93
94
  metadata: { lc_source: "llmToolSelector" },
94
95
  tags: [require_constants.INTERNAL_CALL_TAG],
95
96
  callbacks: []
@@ -1 +1 @@
1
- {"version":3,"file":"llmToolSelector.cjs","names":["z","BaseLanguageModel","createMiddleware","INTERNAL_CALL_TAG","HumanMessage","initChatModel"],"sources":["../../../src/agents/middleware/llmToolSelector.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { BaseLanguageModel } from \"@langchain/core/language_models/base\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\nimport { HumanMessage } from \"@langchain/core/messages\";\nimport type { StructuredToolInterface } from \"@langchain/core/tools\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { INTERNAL_CALL_TAG } from \"./constants.js\";\nimport { initChatModel } from \"../../chat_models/universal.js\";\nimport type { Runtime } from \"../runtime.js\";\nimport type { ModelRequest } from \"../nodes/types.js\";\nimport {\n mergeConfigs,\n pickRunnableConfigKeys,\n type RunnableConfig,\n} from \"@langchain/core/runnables\";\n\nconst DEFAULT_SYSTEM_PROMPT =\n \"Your goal is to select the most relevant tools for answering the user's query.\";\n\n/**\n * Prepared inputs for tool selection.\n */\ninterface SelectionRequest {\n availableTools: StructuredToolInterface[];\n systemMessage: string;\n lastUserMessage: HumanMessage;\n model: BaseLanguageModel;\n validToolNames: string[];\n}\n\n/**\n * Create a structured output schema for tool selection.\n *\n * @param tools - Available tools to include in the schema.\n * @returns Zod schema where each tool name is a literal with its description.\n */\nfunction createToolSelectionResponse(tools: StructuredToolInterface[]) {\n if (!tools || tools.length === 0) {\n throw new Error(\"Invalid usage: tools must be non-empty\");\n }\n\n // Create a union of literals for each tool name\n const toolLiterals = tools.map((tool) => z.literal(tool.name));\n const toolEnum = z.union(\n toolLiterals as [\n z.ZodLiteral<string>,\n z.ZodLiteral<string>,\n ...z.ZodLiteral<string>[],\n ]\n );\n\n return z.object({\n tools: z\n .array(toolEnum)\n .describe(\"Tools to use. Place the most relevant tools first.\"),\n });\n}\n\n/**\n * Options for configuring the LLM Tool Selector middleware.\n */\nexport const LLMToolSelectorOptionsSchema = z.object({\n /**\n * The language model to use for tool selection (default: the provided model from the agent options).\n */\n model: z.string().or(z.instanceof(BaseLanguageModel)).optional(),\n /**\n * System prompt for the tool selection model.\n */\n systemPrompt: z.string().optional(),\n /**\n * Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n */\n maxTools: z.number().optional(),\n /**\n * Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n */\n alwaysInclude: z.array(z.string()).optional(),\n});\nexport type LLMToolSelectorConfig = InferInteropZodInput<\n typeof LLMToolSelectorOptionsSchema\n>;\n\n/**\n * Middleware for selecting tools using an LLM-based strategy.\n *\n * When an agent has many tools available, this middleware filters them down\n * to only the most relevant ones for the user's query. This reduces token usage\n * and helps the main model focus on the right tools.\n *\n * @param options - Configuration options for the middleware\n * @param options.model - The language model to use for tool selection (default: the provided model from the agent options).\n * @param options.systemPrompt - Instructions for the selection model.\n * @param options.maxTools - Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n * @param options.alwaysInclude - Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n *\n * @example\n * Limit to 3 tools:\n * ```ts\n * import { llmToolSelectorMiddleware } from \"langchain/agents/middleware\";\n *\n * const middleware = llmToolSelectorMiddleware({ maxTools: 3 });\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [tool1, tool2, tool3, tool4, tool5],\n * middleware: [middleware],\n * });\n * ```\n *\n * @example\n * Use a smaller model for selection:\n * ```ts\n * const middleware = llmToolSelectorMiddleware({\n * model: \"openai:gpt-4o-mini\",\n * maxTools: 2\n * });\n * ```\n */\nexport function llmToolSelectorMiddleware(options: LLMToolSelectorConfig) {\n return createMiddleware({\n name: \"LLMToolSelector\",\n contextSchema: LLMToolSelectorOptionsSchema,\n async wrapModelCall(request, handler) {\n const selectionRequest = await prepareSelectionRequest(\n request,\n options,\n request.runtime\n );\n if (!selectionRequest) {\n return handler(request);\n }\n\n // Create dynamic response model with union of literal tool names\n const toolSelectionSchema = createToolSelectionResponse(\n selectionRequest.availableTools\n );\n const structuredModel =\n await selectionRequest.model.withStructuredOutput?.(\n toolSelectionSchema\n );\n\n const baseConfig: RunnableConfig =\n pickRunnableConfigKeys(request.runtime) ?? {};\n const config = mergeConfigs(baseConfig, {\n metadata: { lc_source: \"llmToolSelector\" },\n tags: [INTERNAL_CALL_TAG],\n callbacks: [],\n });\n\n const response = await structuredModel?.invoke(\n [\n { role: \"system\", content: selectionRequest.systemMessage },\n selectionRequest.lastUserMessage,\n ],\n config\n );\n\n // Response should be an object with a tools array\n if (!response || typeof response !== \"object\" || !(\"tools\" in response)) {\n throw new Error(\n `Expected object response with tools array, got ${typeof response}`\n );\n }\n\n return handler(\n processSelectionResponse(\n response as { tools: string[] },\n selectionRequest.availableTools,\n selectionRequest.validToolNames,\n request,\n options\n )\n );\n },\n });\n}\n\n/**\n * Prepare inputs for tool selection.\n *\n * @param request - The model request to process.\n * @param options - Configuration options.\n * @param runtime - Runtime context.\n * @returns SelectionRequest with prepared inputs, or null if no selection is needed.\n */\nasync function prepareSelectionRequest<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig,\n runtime: Runtime<LLMToolSelectorConfig>\n): Promise<SelectionRequest | undefined> {\n const model = runtime.context.model ?? options.model;\n const maxTools = runtime.context.maxTools ?? options.maxTools;\n const alwaysInclude =\n runtime.context.alwaysInclude ?? options.alwaysInclude ?? [];\n const systemPrompt =\n runtime.context.systemPrompt ??\n options.systemPrompt ??\n DEFAULT_SYSTEM_PROMPT;\n\n /**\n * If no tools available, return null\n */\n if (!request.tools || request.tools.length === 0) {\n return undefined;\n }\n\n /**\n * Filter to only StructuredToolInterface instances (exclude provider-specific tool dicts)\n */\n const baseTools = request.tools.filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n );\n\n /**\n * Validate that alwaysInclude tools exist\n */\n if (alwaysInclude.length > 0) {\n const availableToolNames = new Set(baseTools.map((tool) => tool.name));\n const missingTools = alwaysInclude.filter(\n (name) => !availableToolNames.has(name)\n );\n if (missingTools.length > 0) {\n throw new Error(\n `Tools in alwaysInclude not found in request: ${missingTools.join(\n \", \"\n )}. ` +\n `Available tools: ${Array.from(availableToolNames).sort().join(\", \")}`\n );\n }\n }\n\n /**\n * Separate tools that are always included from those available for selection\n */\n const availableTools = baseTools.filter(\n (tool) => !alwaysInclude.includes(tool.name)\n );\n\n /**\n * If no tools available for selection, return null\n */\n if (availableTools.length === 0) {\n return undefined;\n }\n\n let systemMessage = systemPrompt;\n /**\n * If there's a maxTools limit, append instructions to the system prompt\n */\n if (maxTools !== undefined) {\n systemMessage +=\n `\\nIMPORTANT: List the tool names in order of relevance, ` +\n `with the most relevant first. ` +\n `If you exceed the maximum number of tools, ` +\n `only the first ${maxTools} will be used.`;\n }\n\n /**\n * Get the last user message from the conversation history\n */\n let lastUserMessage: HumanMessage | undefined;\n for (const message of request.messages) {\n if (HumanMessage.isInstance(message)) {\n lastUserMessage = message;\n }\n }\n\n if (!lastUserMessage) {\n throw new Error(\"No user message found in request messages\");\n }\n\n const modelInstance = !model\n ? (request.model as BaseLanguageModel)\n : typeof model === \"string\"\n ? await initChatModel(model)\n : model;\n\n const validToolNames = availableTools.map((tool) => tool.name);\n\n return {\n availableTools,\n systemMessage,\n lastUserMessage,\n model: modelInstance,\n validToolNames,\n };\n}\n\n/**\n * Process the selection response and return filtered ModelRequest.\n *\n * @param response - The structured output response from the model.\n * @param availableTools - Tools available for selection.\n * @param validToolNames - Valid tool names that can be selected.\n * @param request - Original model request.\n * @param options - Configuration options.\n * @returns Modified ModelRequest with filtered tools.\n */\nfunction processSelectionResponse<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n response: { tools: string[] },\n availableTools: StructuredToolInterface[],\n validToolNames: string[],\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig\n): ModelRequest<TState, TContext> {\n const maxTools = options.maxTools;\n const alwaysInclude = options.alwaysInclude ?? [];\n\n const selectedToolNames: string[] = [];\n const invalidToolSelections: string[] = [];\n\n for (const toolName of response.tools) {\n if (!validToolNames.includes(toolName)) {\n invalidToolSelections.push(toolName);\n continue;\n }\n\n /**\n * Only add if not already selected and within maxTools limit\n */\n if (\n !selectedToolNames.includes(toolName) &&\n (maxTools === undefined || selectedToolNames.length < maxTools)\n ) {\n selectedToolNames.push(toolName);\n }\n }\n\n if (invalidToolSelections.length > 0) {\n throw new Error(\n `Model selected invalid tools: ${invalidToolSelections.join(\", \")}`\n );\n }\n\n /**\n * Filter tools based on selection\n */\n const selectedTools = availableTools.filter((tool) =>\n selectedToolNames.includes(tool.name)\n );\n\n /**\n * Append always-included tools\n */\n const alwaysIncludedTools = (request.tools ?? []).filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n typeof tool.name === \"string\" &&\n alwaysInclude.includes(tool.name)\n );\n selectedTools.push(...alwaysIncludedTools);\n\n /**\n * Also preserve any provider-specific tool dicts from the original request\n */\n const providerTools = (request.tools ?? []).filter(\n (tool) =>\n !(\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n )\n );\n\n return {\n ...request,\n tools: [...selectedTools, ...providerTools],\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"llmToolSelector.cjs","names":["z","BaseLanguageModel","createMiddleware","pickRunnableConfigKeys","mergeConfigs","INTERNAL_CALL_TAG","HumanMessage","initChatModel"],"sources":["../../../src/agents/middleware/llmToolSelector.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { BaseLanguageModel } from \"@langchain/core/language_models/base\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\nimport { HumanMessage } from \"@langchain/core/messages\";\nimport type { StructuredToolInterface } from \"@langchain/core/tools\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { INTERNAL_CALL_TAG } from \"./constants.js\";\nimport { initChatModel } from \"../../chat_models/universal.js\";\nimport type { Runtime } from \"../runtime.js\";\nimport type { ModelRequest } from \"../nodes/types.js\";\nimport {\n mergeConfigs,\n pickRunnableConfigKeys,\n type RunnableConfig,\n} from \"@langchain/core/runnables\";\n\nconst DEFAULT_SYSTEM_PROMPT =\n \"Your goal is to select the most relevant tools for answering the user's query.\";\n\n/**\n * Prepared inputs for tool selection.\n */\ninterface SelectionRequest {\n availableTools: StructuredToolInterface[];\n systemMessage: string;\n lastUserMessage: HumanMessage;\n model: BaseLanguageModel;\n validToolNames: string[];\n}\n\n/**\n * Create a structured output schema for tool selection.\n *\n * @param tools - Available tools to include in the schema.\n * @returns Zod schema where each tool name is a literal with its description.\n */\nfunction createToolSelectionResponse(tools: StructuredToolInterface[]) {\n if (!tools || tools.length === 0) {\n throw new Error(\"Invalid usage: tools must be non-empty\");\n }\n\n // Create a union of literals for each tool name\n const toolLiterals = tools.map((tool) => z.literal(tool.name));\n const toolEnum = z.union(\n toolLiterals as [\n z.ZodLiteral<string>,\n z.ZodLiteral<string>,\n ...z.ZodLiteral<string>[],\n ]\n );\n\n return z.object({\n tools: z\n .array(toolEnum)\n .describe(\"Tools to use. Place the most relevant tools first.\"),\n });\n}\n\n/**\n * Options for configuring the LLM Tool Selector middleware.\n */\nexport const LLMToolSelectorOptionsSchema = z.object({\n /**\n * The language model to use for tool selection (default: the provided model from the agent options).\n */\n model: z.string().or(z.instanceof(BaseLanguageModel)).optional(),\n /**\n * System prompt for the tool selection model.\n */\n systemPrompt: z.string().optional(),\n /**\n * Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n */\n maxTools: z.number().optional(),\n /**\n * Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n */\n alwaysInclude: z.array(z.string()).optional(),\n});\nexport type LLMToolSelectorConfig = InferInteropZodInput<\n typeof LLMToolSelectorOptionsSchema\n>;\n\n/**\n * Middleware for selecting tools using an LLM-based strategy.\n *\n * When an agent has many tools available, this middleware filters them down\n * to only the most relevant ones for the user's query. This reduces token usage\n * and helps the main model focus on the right tools.\n *\n * @param options - Configuration options for the middleware\n * @param options.model - The language model to use for tool selection (default: the provided model from the agent options).\n * @param options.systemPrompt - Instructions for the selection model.\n * @param options.maxTools - Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n * @param options.alwaysInclude - Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n *\n * @example\n * Limit to 3 tools:\n * ```ts\n * import { llmToolSelectorMiddleware } from \"langchain/agents/middleware\";\n *\n * const middleware = llmToolSelectorMiddleware({ maxTools: 3 });\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [tool1, tool2, tool3, tool4, tool5],\n * middleware: [middleware],\n * });\n * ```\n *\n * @example\n * Use a smaller model for selection:\n * ```ts\n * const middleware = llmToolSelectorMiddleware({\n * model: \"openai:gpt-4o-mini\",\n * maxTools: 2\n * });\n * ```\n */\nexport function llmToolSelectorMiddleware(options: LLMToolSelectorConfig) {\n return createMiddleware({\n name: \"LLMToolSelector\",\n contextSchema: LLMToolSelectorOptionsSchema,\n async wrapModelCall(request, handler) {\n const selectionRequest = await prepareSelectionRequest(\n request,\n options,\n request.runtime\n );\n if (!selectionRequest) {\n return handler(request);\n }\n\n // Create dynamic response model with union of literal tool names\n const toolSelectionSchema = createToolSelectionResponse(\n selectionRequest.availableTools\n );\n const structuredModel =\n await selectionRequest.model.withStructuredOutput?.(\n toolSelectionSchema\n );\n\n const baseConfig: RunnableConfig =\n pickRunnableConfigKeys(request.runtime) ?? {};\n const config = mergeConfigs(baseConfig, {\n metadata: { lc_source: \"llmToolSelector\" },\n tags: [INTERNAL_CALL_TAG],\n callbacks: [],\n });\n\n const response = await structuredModel?.invoke(\n [\n { role: \"system\", content: selectionRequest.systemMessage },\n selectionRequest.lastUserMessage,\n ],\n config\n );\n\n // Response should be an object with a tools array\n if (!response || typeof response !== \"object\" || !(\"tools\" in response)) {\n throw new Error(\n `Expected object response with tools array, got ${typeof response}`\n );\n }\n\n return handler(\n processSelectionResponse(\n response as { tools: string[] },\n selectionRequest.availableTools,\n selectionRequest.validToolNames,\n request,\n options\n )\n );\n },\n });\n}\n\n/**\n * Prepare inputs for tool selection.\n *\n * @param request - The model request to process.\n * @param options - Configuration options.\n * @param runtime - Runtime context.\n * @returns SelectionRequest with prepared inputs, or null if no selection is needed.\n */\nasync function prepareSelectionRequest<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig,\n runtime: Runtime<LLMToolSelectorConfig>\n): Promise<SelectionRequest | undefined> {\n const model = runtime.context.model ?? options.model;\n const maxTools = runtime.context.maxTools ?? options.maxTools;\n const alwaysInclude =\n runtime.context.alwaysInclude ?? options.alwaysInclude ?? [];\n const systemPrompt =\n runtime.context.systemPrompt ??\n options.systemPrompt ??\n DEFAULT_SYSTEM_PROMPT;\n\n /**\n * If no tools available, return null\n */\n if (!request.tools || request.tools.length === 0) {\n return undefined;\n }\n\n /**\n * Filter to only StructuredToolInterface instances (exclude provider-specific tool dicts)\n */\n const baseTools = request.tools.filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n );\n\n /**\n * Validate that alwaysInclude tools exist\n */\n if (alwaysInclude.length > 0) {\n const availableToolNames = new Set(baseTools.map((tool) => tool.name));\n const missingTools = alwaysInclude.filter(\n (name) => !availableToolNames.has(name)\n );\n if (missingTools.length > 0) {\n throw new Error(\n `Tools in alwaysInclude not found in request: ${missingTools.join(\n \", \"\n )}. ` +\n `Available tools: ${Array.from(availableToolNames).sort().join(\", \")}`\n );\n }\n }\n\n /**\n * Separate tools that are always included from those available for selection\n */\n const availableTools = baseTools.filter(\n (tool) => !alwaysInclude.includes(tool.name)\n );\n\n /**\n * If no tools available for selection, return null\n */\n if (availableTools.length === 0) {\n return undefined;\n }\n\n let systemMessage = systemPrompt;\n /**\n * If there's a maxTools limit, append instructions to the system prompt\n */\n if (maxTools !== undefined) {\n systemMessage +=\n `\\nIMPORTANT: List the tool names in order of relevance, ` +\n `with the most relevant first. ` +\n `If you exceed the maximum number of tools, ` +\n `only the first ${maxTools} will be used.`;\n }\n\n /**\n * Get the last user message from the conversation history\n */\n let lastUserMessage: HumanMessage | undefined;\n for (const message of request.messages) {\n if (HumanMessage.isInstance(message)) {\n lastUserMessage = message;\n }\n }\n\n if (!lastUserMessage) {\n throw new Error(\"No user message found in request messages\");\n }\n\n const modelInstance = !model\n ? (request.model as BaseLanguageModel)\n : typeof model === \"string\"\n ? await initChatModel(model)\n : model;\n\n const validToolNames = availableTools.map((tool) => tool.name);\n\n return {\n availableTools,\n systemMessage,\n lastUserMessage,\n model: modelInstance,\n validToolNames,\n };\n}\n\n/**\n * Process the selection response and return filtered ModelRequest.\n *\n * @param response - The structured output response from the model.\n * @param availableTools - Tools available for selection.\n * @param validToolNames - Valid tool names that can be selected.\n * @param request - Original model request.\n * @param options - Configuration options.\n * @returns Modified ModelRequest with filtered tools.\n */\nfunction processSelectionResponse<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n response: { tools: string[] },\n availableTools: StructuredToolInterface[],\n validToolNames: string[],\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig\n): ModelRequest<TState, TContext> {\n const maxTools = options.maxTools;\n const alwaysInclude = options.alwaysInclude ?? [];\n\n const selectedToolNames: string[] = [];\n const invalidToolSelections: string[] = [];\n\n for (const toolName of response.tools) {\n if (!validToolNames.includes(toolName)) {\n invalidToolSelections.push(toolName);\n continue;\n }\n\n /**\n * Only add if not already selected and within maxTools limit\n */\n if (\n !selectedToolNames.includes(toolName) &&\n (maxTools === undefined || selectedToolNames.length < maxTools)\n ) {\n selectedToolNames.push(toolName);\n }\n }\n\n if (invalidToolSelections.length > 0) {\n throw new Error(\n `Model selected invalid tools: ${invalidToolSelections.join(\", \")}`\n );\n }\n\n /**\n * Filter tools based on selection\n */\n const selectedTools = availableTools.filter((tool) =>\n selectedToolNames.includes(tool.name)\n );\n\n /**\n * Append always-included tools\n */\n const alwaysIncludedTools = (request.tools ?? []).filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n typeof tool.name === \"string\" &&\n alwaysInclude.includes(tool.name)\n );\n selectedTools.push(...alwaysIncludedTools);\n\n /**\n * Also preserve any provider-specific tool dicts from the original request\n */\n const providerTools = (request.tools ?? []).filter(\n (tool) =>\n !(\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n )\n );\n\n return {\n ...request,\n tools: [...selectedTools, ...providerTools],\n };\n}\n"],"mappings":"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"}
@@ -89,7 +89,8 @@ function llmToolSelectorMiddleware(options) {
89
89
  if (!selectionRequest) return handler(request);
90
90
  const toolSelectionSchema = createToolSelectionResponse(selectionRequest.availableTools);
91
91
  const structuredModel = await selectionRequest.model.withStructuredOutput?.(toolSelectionSchema);
92
- const config = mergeConfigs(pickRunnableConfigKeys(request.runtime) ?? {}, {
92
+ const baseConfig = pickRunnableConfigKeys(request.runtime) ?? {};
93
+ const config = mergeConfigs(baseConfig, {
93
94
  metadata: { lc_source: "llmToolSelector" },
94
95
  tags: [INTERNAL_CALL_TAG],
95
96
  callbacks: []
@@ -1 +1 @@
1
- {"version":3,"file":"llmToolSelector.js","names":[],"sources":["../../../src/agents/middleware/llmToolSelector.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { BaseLanguageModel } from \"@langchain/core/language_models/base\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\nimport { HumanMessage } from \"@langchain/core/messages\";\nimport type { StructuredToolInterface } from \"@langchain/core/tools\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { INTERNAL_CALL_TAG } from \"./constants.js\";\nimport { initChatModel } from \"../../chat_models/universal.js\";\nimport type { Runtime } from \"../runtime.js\";\nimport type { ModelRequest } from \"../nodes/types.js\";\nimport {\n mergeConfigs,\n pickRunnableConfigKeys,\n type RunnableConfig,\n} from \"@langchain/core/runnables\";\n\nconst DEFAULT_SYSTEM_PROMPT =\n \"Your goal is to select the most relevant tools for answering the user's query.\";\n\n/**\n * Prepared inputs for tool selection.\n */\ninterface SelectionRequest {\n availableTools: StructuredToolInterface[];\n systemMessage: string;\n lastUserMessage: HumanMessage;\n model: BaseLanguageModel;\n validToolNames: string[];\n}\n\n/**\n * Create a structured output schema for tool selection.\n *\n * @param tools - Available tools to include in the schema.\n * @returns Zod schema where each tool name is a literal with its description.\n */\nfunction createToolSelectionResponse(tools: StructuredToolInterface[]) {\n if (!tools || tools.length === 0) {\n throw new Error(\"Invalid usage: tools must be non-empty\");\n }\n\n // Create a union of literals for each tool name\n const toolLiterals = tools.map((tool) => z.literal(tool.name));\n const toolEnum = z.union(\n toolLiterals as [\n z.ZodLiteral<string>,\n z.ZodLiteral<string>,\n ...z.ZodLiteral<string>[],\n ]\n );\n\n return z.object({\n tools: z\n .array(toolEnum)\n .describe(\"Tools to use. Place the most relevant tools first.\"),\n });\n}\n\n/**\n * Options for configuring the LLM Tool Selector middleware.\n */\nexport const LLMToolSelectorOptionsSchema = z.object({\n /**\n * The language model to use for tool selection (default: the provided model from the agent options).\n */\n model: z.string().or(z.instanceof(BaseLanguageModel)).optional(),\n /**\n * System prompt for the tool selection model.\n */\n systemPrompt: z.string().optional(),\n /**\n * Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n */\n maxTools: z.number().optional(),\n /**\n * Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n */\n alwaysInclude: z.array(z.string()).optional(),\n});\nexport type LLMToolSelectorConfig = InferInteropZodInput<\n typeof LLMToolSelectorOptionsSchema\n>;\n\n/**\n * Middleware for selecting tools using an LLM-based strategy.\n *\n * When an agent has many tools available, this middleware filters them down\n * to only the most relevant ones for the user's query. This reduces token usage\n * and helps the main model focus on the right tools.\n *\n * @param options - Configuration options for the middleware\n * @param options.model - The language model to use for tool selection (default: the provided model from the agent options).\n * @param options.systemPrompt - Instructions for the selection model.\n * @param options.maxTools - Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n * @param options.alwaysInclude - Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n *\n * @example\n * Limit to 3 tools:\n * ```ts\n * import { llmToolSelectorMiddleware } from \"langchain/agents/middleware\";\n *\n * const middleware = llmToolSelectorMiddleware({ maxTools: 3 });\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [tool1, tool2, tool3, tool4, tool5],\n * middleware: [middleware],\n * });\n * ```\n *\n * @example\n * Use a smaller model for selection:\n * ```ts\n * const middleware = llmToolSelectorMiddleware({\n * model: \"openai:gpt-4o-mini\",\n * maxTools: 2\n * });\n * ```\n */\nexport function llmToolSelectorMiddleware(options: LLMToolSelectorConfig) {\n return createMiddleware({\n name: \"LLMToolSelector\",\n contextSchema: LLMToolSelectorOptionsSchema,\n async wrapModelCall(request, handler) {\n const selectionRequest = await prepareSelectionRequest(\n request,\n options,\n request.runtime\n );\n if (!selectionRequest) {\n return handler(request);\n }\n\n // Create dynamic response model with union of literal tool names\n const toolSelectionSchema = createToolSelectionResponse(\n selectionRequest.availableTools\n );\n const structuredModel =\n await selectionRequest.model.withStructuredOutput?.(\n toolSelectionSchema\n );\n\n const baseConfig: RunnableConfig =\n pickRunnableConfigKeys(request.runtime) ?? {};\n const config = mergeConfigs(baseConfig, {\n metadata: { lc_source: \"llmToolSelector\" },\n tags: [INTERNAL_CALL_TAG],\n callbacks: [],\n });\n\n const response = await structuredModel?.invoke(\n [\n { role: \"system\", content: selectionRequest.systemMessage },\n selectionRequest.lastUserMessage,\n ],\n config\n );\n\n // Response should be an object with a tools array\n if (!response || typeof response !== \"object\" || !(\"tools\" in response)) {\n throw new Error(\n `Expected object response with tools array, got ${typeof response}`\n );\n }\n\n return handler(\n processSelectionResponse(\n response as { tools: string[] },\n selectionRequest.availableTools,\n selectionRequest.validToolNames,\n request,\n options\n )\n );\n },\n });\n}\n\n/**\n * Prepare inputs for tool selection.\n *\n * @param request - The model request to process.\n * @param options - Configuration options.\n * @param runtime - Runtime context.\n * @returns SelectionRequest with prepared inputs, or null if no selection is needed.\n */\nasync function prepareSelectionRequest<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig,\n runtime: Runtime<LLMToolSelectorConfig>\n): Promise<SelectionRequest | undefined> {\n const model = runtime.context.model ?? options.model;\n const maxTools = runtime.context.maxTools ?? options.maxTools;\n const alwaysInclude =\n runtime.context.alwaysInclude ?? options.alwaysInclude ?? [];\n const systemPrompt =\n runtime.context.systemPrompt ??\n options.systemPrompt ??\n DEFAULT_SYSTEM_PROMPT;\n\n /**\n * If no tools available, return null\n */\n if (!request.tools || request.tools.length === 0) {\n return undefined;\n }\n\n /**\n * Filter to only StructuredToolInterface instances (exclude provider-specific tool dicts)\n */\n const baseTools = request.tools.filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n );\n\n /**\n * Validate that alwaysInclude tools exist\n */\n if (alwaysInclude.length > 0) {\n const availableToolNames = new Set(baseTools.map((tool) => tool.name));\n const missingTools = alwaysInclude.filter(\n (name) => !availableToolNames.has(name)\n );\n if (missingTools.length > 0) {\n throw new Error(\n `Tools in alwaysInclude not found in request: ${missingTools.join(\n \", \"\n )}. ` +\n `Available tools: ${Array.from(availableToolNames).sort().join(\", \")}`\n );\n }\n }\n\n /**\n * Separate tools that are always included from those available for selection\n */\n const availableTools = baseTools.filter(\n (tool) => !alwaysInclude.includes(tool.name)\n );\n\n /**\n * If no tools available for selection, return null\n */\n if (availableTools.length === 0) {\n return undefined;\n }\n\n let systemMessage = systemPrompt;\n /**\n * If there's a maxTools limit, append instructions to the system prompt\n */\n if (maxTools !== undefined) {\n systemMessage +=\n `\\nIMPORTANT: List the tool names in order of relevance, ` +\n `with the most relevant first. ` +\n `If you exceed the maximum number of tools, ` +\n `only the first ${maxTools} will be used.`;\n }\n\n /**\n * Get the last user message from the conversation history\n */\n let lastUserMessage: HumanMessage | undefined;\n for (const message of request.messages) {\n if (HumanMessage.isInstance(message)) {\n lastUserMessage = message;\n }\n }\n\n if (!lastUserMessage) {\n throw new Error(\"No user message found in request messages\");\n }\n\n const modelInstance = !model\n ? (request.model as BaseLanguageModel)\n : typeof model === \"string\"\n ? await initChatModel(model)\n : model;\n\n const validToolNames = availableTools.map((tool) => tool.name);\n\n return {\n availableTools,\n systemMessage,\n lastUserMessage,\n model: modelInstance,\n validToolNames,\n };\n}\n\n/**\n * Process the selection response and return filtered ModelRequest.\n *\n * @param response - The structured output response from the model.\n * @param availableTools - Tools available for selection.\n * @param validToolNames - Valid tool names that can be selected.\n * @param request - Original model request.\n * @param options - Configuration options.\n * @returns Modified ModelRequest with filtered tools.\n */\nfunction processSelectionResponse<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n response: { tools: string[] },\n availableTools: StructuredToolInterface[],\n validToolNames: string[],\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig\n): ModelRequest<TState, TContext> {\n const maxTools = options.maxTools;\n const alwaysInclude = options.alwaysInclude ?? [];\n\n const selectedToolNames: string[] = [];\n const invalidToolSelections: string[] = [];\n\n for (const toolName of response.tools) {\n if (!validToolNames.includes(toolName)) {\n invalidToolSelections.push(toolName);\n continue;\n }\n\n /**\n * Only add if not already selected and within maxTools limit\n */\n if (\n !selectedToolNames.includes(toolName) &&\n (maxTools === undefined || selectedToolNames.length < maxTools)\n ) {\n selectedToolNames.push(toolName);\n }\n }\n\n if (invalidToolSelections.length > 0) {\n throw new Error(\n `Model selected invalid tools: ${invalidToolSelections.join(\", \")}`\n );\n }\n\n /**\n * Filter tools based on selection\n */\n const selectedTools = availableTools.filter((tool) =>\n selectedToolNames.includes(tool.name)\n );\n\n /**\n * Append always-included tools\n */\n const alwaysIncludedTools = (request.tools ?? []).filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n typeof tool.name === \"string\" &&\n alwaysInclude.includes(tool.name)\n );\n selectedTools.push(...alwaysIncludedTools);\n\n /**\n * Also preserve any provider-specific tool dicts from the original request\n */\n const providerTools = (request.tools ?? []).filter(\n (tool) =>\n !(\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n )\n );\n\n return {\n ...request,\n tools: [...selectedTools, ...providerTools],\n };\n}\n"],"mappings":";;;;;;;;AAiBA,MAAM,wBACJ;;;;;;;AAmBF,SAAS,4BAA4B,OAAkC;CACrE,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B,MAAM,IAAI,MAAM,wCAAwC;CAI1D,MAAM,eAAe,MAAM,KAAK,SAAS,EAAE,QAAQ,KAAK,IAAI,CAAC;CAC7D,MAAM,WAAW,EAAE,MACjB,YAKF;CAEA,OAAO,EAAE,OAAO,EACd,OAAO,EACJ,MAAM,QAAQ,CAAC,CACf,SAAS,oDAAoD,EAClE,CAAC;AACH;;;;AAKA,MAAa,+BAA+B,EAAE,OAAO;;;;CAInD,OAAO,EAAE,OAAO,CAAC,CAAC,GAAG,EAAE,WAAW,iBAAiB,CAAC,CAAC,CAAC,SAAS;;;;CAI/D,cAAc,EAAE,OAAO,CAAC,CAAC,SAAS;;;;;CAKlC,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;;;;;CAK9B,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS;AAC9C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CD,SAAgB,0BAA0B,SAAgC;CACxE,OAAO,iBAAiB;EACtB,MAAM;EACN,eAAe;EACf,MAAM,cAAc,SAAS,SAAS;GACpC,MAAM,mBAAmB,MAAM,wBAC7B,SACA,SACA,QAAQ,OACV;GACA,IAAI,CAAC,kBACH,OAAO,QAAQ,OAAO;GAIxB,MAAM,sBAAsB,4BAC1B,iBAAiB,cACnB;GACA,MAAM,kBACJ,MAAM,iBAAiB,MAAM,uBAC3B,mBACF;GAIF,MAAM,SAAS,aADb,uBAAuB,QAAQ,OAAO,KAAK,CAAC,GACN;IACtC,UAAU,EAAE,WAAW,kBAAkB;IACzC,MAAM,CAAC,iBAAiB;IACxB,WAAW,CAAC;GACd,CAAC;GAED,MAAM,WAAW,MAAM,iBAAiB,OACtC,CACE;IAAE,MAAM;IAAU,SAAS,iBAAiB;GAAc,GAC1D,iBAAiB,eACnB,GACA,MACF;GAGA,IAAI,CAAC,YAAY,OAAO,aAAa,YAAY,EAAE,WAAW,WAC5D,MAAM,IAAI,MACR,kDAAkD,OAAO,UAC3D;GAGF,OAAO,QACL,yBACE,UACA,iBAAiB,gBACjB,iBAAiB,gBACjB,SACA,OACF,CACF;EACF;CACF,CAAC;AACH;;;;;;;;;AAUA,eAAe,wBAIb,SACA,SACA,SACuC;CACvC,MAAM,QAAQ,QAAQ,QAAQ,SAAS,QAAQ;CAC/C,MAAM,WAAW,QAAQ,QAAQ,YAAY,QAAQ;CACrD,MAAM,gBACJ,QAAQ,QAAQ,iBAAiB,QAAQ,iBAAiB,CAAC;CAC7D,MAAM,eACJ,QAAQ,QAAQ,gBAChB,QAAQ,gBACR;;;;CAKF,IAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,WAAW,GAC7C;;;;CAMF,MAAM,YAAY,QAAQ,MAAM,QAC7B,SACC,OAAO,SAAS,YAChB,UAAU,QACV,iBAAiB,QACjB,OAAO,KAAK,SAAS,QACzB;;;;CAKA,IAAI,cAAc,SAAS,GAAG;EAC5B,MAAM,qBAAqB,IAAI,IAAI,UAAU,KAAK,SAAS,KAAK,IAAI,CAAC;EACrE,MAAM,eAAe,cAAc,QAChC,SAAS,CAAC,mBAAmB,IAAI,IAAI,CACxC;EACA,IAAI,aAAa,SAAS,GACxB,MAAM,IAAI,MACR,gDAAgD,aAAa,KAC3D,IACF,EAAE,qBACoB,MAAM,KAAK,kBAAkB,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,GACvE;CAEJ;;;;CAKA,MAAM,iBAAiB,UAAU,QAC9B,SAAS,CAAC,cAAc,SAAS,KAAK,IAAI,CAC7C;;;;CAKA,IAAI,eAAe,WAAW,GAC5B;CAGF,IAAI,gBAAgB;;;;CAIpB,IAAI,aAAa,KAAA,GACf,iBACE;gJAGkB,SAAS;;;;CAM/B,IAAI;CACJ,KAAK,MAAM,WAAW,QAAQ,UAC5B,IAAI,aAAa,WAAW,OAAO,GACjC,kBAAkB;CAItB,IAAI,CAAC,iBACH,MAAM,IAAI,MAAM,2CAA2C;CAG7D,MAAM,gBAAgB,CAAC,QAClB,QAAQ,QACT,OAAO,UAAU,WACf,MAAM,cAAc,KAAK,IACzB;CAEN,MAAM,iBAAiB,eAAe,KAAK,SAAS,KAAK,IAAI;CAE7D,OAAO;EACL;EACA;EACA;EACA,OAAO;EACP;CACF;AACF;;;;;;;;;;;AAYA,SAAS,yBAIP,UACA,gBACA,gBACA,SACA,SACgC;CAChC,MAAM,WAAW,QAAQ;CACzB,MAAM,gBAAgB,QAAQ,iBAAiB,CAAC;CAEhD,MAAM,oBAA8B,CAAC;CACrC,MAAM,wBAAkC,CAAC;CAEzC,KAAK,MAAM,YAAY,SAAS,OAAO;EACrC,IAAI,CAAC,eAAe,SAAS,QAAQ,GAAG;GACtC,sBAAsB,KAAK,QAAQ;GACnC;EACF;;;;EAKA,IACE,CAAC,kBAAkB,SAAS,QAAQ,MACnC,aAAa,KAAA,KAAa,kBAAkB,SAAS,WAEtD,kBAAkB,KAAK,QAAQ;CAEnC;CAEA,IAAI,sBAAsB,SAAS,GACjC,MAAM,IAAI,MACR,iCAAiC,sBAAsB,KAAK,IAAI,GAClE;;;;CAMF,MAAM,gBAAgB,eAAe,QAAQ,SAC3C,kBAAkB,SAAS,KAAK,IAAI,CACtC;;;;CAKA,MAAM,uBAAuB,QAAQ,SAAS,CAAC,EAAA,CAAG,QAC/C,SACC,OAAO,SAAS,YAChB,UAAU,QACV,OAAO,KAAK,SAAS,YACrB,cAAc,SAAS,KAAK,IAAI,CACpC;CACA,cAAc,KAAK,GAAG,mBAAmB;;;;CAKzC,MAAM,iBAAiB,QAAQ,SAAS,CAAC,EAAA,CAAG,QACzC,SACC,EACE,OAAO,SAAS,YAChB,UAAU,QACV,iBAAiB,QACjB,OAAO,KAAK,SAAS,SAE3B;CAEA,OAAO;EACL,GAAG;EACH,OAAO,CAAC,GAAG,eAAe,GAAG,aAAa;CAC5C;AACF"}
1
+ {"version":3,"file":"llmToolSelector.js","names":[],"sources":["../../../src/agents/middleware/llmToolSelector.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { BaseLanguageModel } from \"@langchain/core/language_models/base\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\nimport { HumanMessage } from \"@langchain/core/messages\";\nimport type { StructuredToolInterface } from \"@langchain/core/tools\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { INTERNAL_CALL_TAG } from \"./constants.js\";\nimport { initChatModel } from \"../../chat_models/universal.js\";\nimport type { Runtime } from \"../runtime.js\";\nimport type { ModelRequest } from \"../nodes/types.js\";\nimport {\n mergeConfigs,\n pickRunnableConfigKeys,\n type RunnableConfig,\n} from \"@langchain/core/runnables\";\n\nconst DEFAULT_SYSTEM_PROMPT =\n \"Your goal is to select the most relevant tools for answering the user's query.\";\n\n/**\n * Prepared inputs for tool selection.\n */\ninterface SelectionRequest {\n availableTools: StructuredToolInterface[];\n systemMessage: string;\n lastUserMessage: HumanMessage;\n model: BaseLanguageModel;\n validToolNames: string[];\n}\n\n/**\n * Create a structured output schema for tool selection.\n *\n * @param tools - Available tools to include in the schema.\n * @returns Zod schema where each tool name is a literal with its description.\n */\nfunction createToolSelectionResponse(tools: StructuredToolInterface[]) {\n if (!tools || tools.length === 0) {\n throw new Error(\"Invalid usage: tools must be non-empty\");\n }\n\n // Create a union of literals for each tool name\n const toolLiterals = tools.map((tool) => z.literal(tool.name));\n const toolEnum = z.union(\n toolLiterals as [\n z.ZodLiteral<string>,\n z.ZodLiteral<string>,\n ...z.ZodLiteral<string>[],\n ]\n );\n\n return z.object({\n tools: z\n .array(toolEnum)\n .describe(\"Tools to use. Place the most relevant tools first.\"),\n });\n}\n\n/**\n * Options for configuring the LLM Tool Selector middleware.\n */\nexport const LLMToolSelectorOptionsSchema = z.object({\n /**\n * The language model to use for tool selection (default: the provided model from the agent options).\n */\n model: z.string().or(z.instanceof(BaseLanguageModel)).optional(),\n /**\n * System prompt for the tool selection model.\n */\n systemPrompt: z.string().optional(),\n /**\n * Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n */\n maxTools: z.number().optional(),\n /**\n * Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n */\n alwaysInclude: z.array(z.string()).optional(),\n});\nexport type LLMToolSelectorConfig = InferInteropZodInput<\n typeof LLMToolSelectorOptionsSchema\n>;\n\n/**\n * Middleware for selecting tools using an LLM-based strategy.\n *\n * When an agent has many tools available, this middleware filters them down\n * to only the most relevant ones for the user's query. This reduces token usage\n * and helps the main model focus on the right tools.\n *\n * @param options - Configuration options for the middleware\n * @param options.model - The language model to use for tool selection (default: the provided model from the agent options).\n * @param options.systemPrompt - Instructions for the selection model.\n * @param options.maxTools - Maximum number of tools to select. If the model selects more,\n * only the first maxTools will be used. No limit if not specified.\n * @param options.alwaysInclude - Tool names to always include regardless of selection.\n * These do not count against the maxTools limit.\n *\n * @example\n * Limit to 3 tools:\n * ```ts\n * import { llmToolSelectorMiddleware } from \"langchain/agents/middleware\";\n *\n * const middleware = llmToolSelectorMiddleware({ maxTools: 3 });\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [tool1, tool2, tool3, tool4, tool5],\n * middleware: [middleware],\n * });\n * ```\n *\n * @example\n * Use a smaller model for selection:\n * ```ts\n * const middleware = llmToolSelectorMiddleware({\n * model: \"openai:gpt-4o-mini\",\n * maxTools: 2\n * });\n * ```\n */\nexport function llmToolSelectorMiddleware(options: LLMToolSelectorConfig) {\n return createMiddleware({\n name: \"LLMToolSelector\",\n contextSchema: LLMToolSelectorOptionsSchema,\n async wrapModelCall(request, handler) {\n const selectionRequest = await prepareSelectionRequest(\n request,\n options,\n request.runtime\n );\n if (!selectionRequest) {\n return handler(request);\n }\n\n // Create dynamic response model with union of literal tool names\n const toolSelectionSchema = createToolSelectionResponse(\n selectionRequest.availableTools\n );\n const structuredModel =\n await selectionRequest.model.withStructuredOutput?.(\n toolSelectionSchema\n );\n\n const baseConfig: RunnableConfig =\n pickRunnableConfigKeys(request.runtime) ?? {};\n const config = mergeConfigs(baseConfig, {\n metadata: { lc_source: \"llmToolSelector\" },\n tags: [INTERNAL_CALL_TAG],\n callbacks: [],\n });\n\n const response = await structuredModel?.invoke(\n [\n { role: \"system\", content: selectionRequest.systemMessage },\n selectionRequest.lastUserMessage,\n ],\n config\n );\n\n // Response should be an object with a tools array\n if (!response || typeof response !== \"object\" || !(\"tools\" in response)) {\n throw new Error(\n `Expected object response with tools array, got ${typeof response}`\n );\n }\n\n return handler(\n processSelectionResponse(\n response as { tools: string[] },\n selectionRequest.availableTools,\n selectionRequest.validToolNames,\n request,\n options\n )\n );\n },\n });\n}\n\n/**\n * Prepare inputs for tool selection.\n *\n * @param request - The model request to process.\n * @param options - Configuration options.\n * @param runtime - Runtime context.\n * @returns SelectionRequest with prepared inputs, or null if no selection is needed.\n */\nasync function prepareSelectionRequest<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig,\n runtime: Runtime<LLMToolSelectorConfig>\n): Promise<SelectionRequest | undefined> {\n const model = runtime.context.model ?? options.model;\n const maxTools = runtime.context.maxTools ?? options.maxTools;\n const alwaysInclude =\n runtime.context.alwaysInclude ?? options.alwaysInclude ?? [];\n const systemPrompt =\n runtime.context.systemPrompt ??\n options.systemPrompt ??\n DEFAULT_SYSTEM_PROMPT;\n\n /**\n * If no tools available, return null\n */\n if (!request.tools || request.tools.length === 0) {\n return undefined;\n }\n\n /**\n * Filter to only StructuredToolInterface instances (exclude provider-specific tool dicts)\n */\n const baseTools = request.tools.filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n );\n\n /**\n * Validate that alwaysInclude tools exist\n */\n if (alwaysInclude.length > 0) {\n const availableToolNames = new Set(baseTools.map((tool) => tool.name));\n const missingTools = alwaysInclude.filter(\n (name) => !availableToolNames.has(name)\n );\n if (missingTools.length > 0) {\n throw new Error(\n `Tools in alwaysInclude not found in request: ${missingTools.join(\n \", \"\n )}. ` +\n `Available tools: ${Array.from(availableToolNames).sort().join(\", \")}`\n );\n }\n }\n\n /**\n * Separate tools that are always included from those available for selection\n */\n const availableTools = baseTools.filter(\n (tool) => !alwaysInclude.includes(tool.name)\n );\n\n /**\n * If no tools available for selection, return null\n */\n if (availableTools.length === 0) {\n return undefined;\n }\n\n let systemMessage = systemPrompt;\n /**\n * If there's a maxTools limit, append instructions to the system prompt\n */\n if (maxTools !== undefined) {\n systemMessage +=\n `\\nIMPORTANT: List the tool names in order of relevance, ` +\n `with the most relevant first. ` +\n `If you exceed the maximum number of tools, ` +\n `only the first ${maxTools} will be used.`;\n }\n\n /**\n * Get the last user message from the conversation history\n */\n let lastUserMessage: HumanMessage | undefined;\n for (const message of request.messages) {\n if (HumanMessage.isInstance(message)) {\n lastUserMessage = message;\n }\n }\n\n if (!lastUserMessage) {\n throw new Error(\"No user message found in request messages\");\n }\n\n const modelInstance = !model\n ? (request.model as BaseLanguageModel)\n : typeof model === \"string\"\n ? await initChatModel(model)\n : model;\n\n const validToolNames = availableTools.map((tool) => tool.name);\n\n return {\n availableTools,\n systemMessage,\n lastUserMessage,\n model: modelInstance,\n validToolNames,\n };\n}\n\n/**\n * Process the selection response and return filtered ModelRequest.\n *\n * @param response - The structured output response from the model.\n * @param availableTools - Tools available for selection.\n * @param validToolNames - Valid tool names that can be selected.\n * @param request - Original model request.\n * @param options - Configuration options.\n * @returns Modified ModelRequest with filtered tools.\n */\nfunction processSelectionResponse<\n TState extends Record<string, unknown> = Record<string, unknown>,\n TContext = unknown,\n>(\n response: { tools: string[] },\n availableTools: StructuredToolInterface[],\n validToolNames: string[],\n request: ModelRequest<TState, TContext>,\n options: LLMToolSelectorConfig\n): ModelRequest<TState, TContext> {\n const maxTools = options.maxTools;\n const alwaysInclude = options.alwaysInclude ?? [];\n\n const selectedToolNames: string[] = [];\n const invalidToolSelections: string[] = [];\n\n for (const toolName of response.tools) {\n if (!validToolNames.includes(toolName)) {\n invalidToolSelections.push(toolName);\n continue;\n }\n\n /**\n * Only add if not already selected and within maxTools limit\n */\n if (\n !selectedToolNames.includes(toolName) &&\n (maxTools === undefined || selectedToolNames.length < maxTools)\n ) {\n selectedToolNames.push(toolName);\n }\n }\n\n if (invalidToolSelections.length > 0) {\n throw new Error(\n `Model selected invalid tools: ${invalidToolSelections.join(\", \")}`\n );\n }\n\n /**\n * Filter tools based on selection\n */\n const selectedTools = availableTools.filter((tool) =>\n selectedToolNames.includes(tool.name)\n );\n\n /**\n * Append always-included tools\n */\n const alwaysIncludedTools = (request.tools ?? []).filter(\n (tool): tool is StructuredToolInterface =>\n typeof tool === \"object\" &&\n \"name\" in tool &&\n typeof tool.name === \"string\" &&\n alwaysInclude.includes(tool.name)\n );\n selectedTools.push(...alwaysIncludedTools);\n\n /**\n * Also preserve any provider-specific tool dicts from the original request\n */\n const providerTools = (request.tools ?? []).filter(\n (tool) =>\n !(\n typeof tool === \"object\" &&\n \"name\" in tool &&\n \"description\" in tool &&\n typeof tool.name === \"string\"\n )\n );\n\n return {\n ...request,\n tools: [...selectedTools, ...providerTools],\n };\n}\n"],"mappings":"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"}
@@ -138,24 +138,25 @@ const BUILT_IN_DETECTORS = {
138
138
  */
139
139
  function resolveRedactionRule(config) {
140
140
  let detector;
141
- if (config.detector) if (typeof config.detector === "string") {
142
- const regex = new RegExp(config.detector, "g");
143
- detector = (content) => {
141
+ if (config.detector) {
142
+ if (typeof config.detector === "string") {
143
+ const regex = new RegExp(config.detector, "g");
144
+ detector = (content) => {
145
+ const matches = [];
146
+ let match;
147
+ const regexCopy = new RegExp(regex);
148
+ while ((match = regexCopy.exec(content)) !== null) matches.push(regexMatchToPIIMatch(match));
149
+ return matches;
150
+ };
151
+ } else if (config.detector instanceof RegExp) detector = (content) => {
152
+ if (!(config.detector instanceof RegExp)) throw new Error("Detector is required");
144
153
  const matches = [];
145
154
  let match;
146
- const regexCopy = new RegExp(regex);
147
- while ((match = regexCopy.exec(content)) !== null) matches.push(regexMatchToPIIMatch(match));
155
+ while ((match = config.detector.exec(content)) !== null) matches.push(regexMatchToPIIMatch(match));
148
156
  return matches;
149
157
  };
150
- } else if (config.detector instanceof RegExp) detector = (content) => {
151
- if (!(config.detector instanceof RegExp)) throw new Error("Detector is required");
152
- const matches = [];
153
- let match;
154
- while ((match = config.detector.exec(content)) !== null) matches.push(regexMatchToPIIMatch(match));
155
- return matches;
156
- };
157
- else detector = config.detector;
158
- else {
158
+ else detector = config.detector;
159
+ } else {
159
160
  const builtInType = config.piiType;
160
161
  if (!BUILT_IN_DETECTORS[builtInType]) throw new Error(`Unknown PII type: ${config.piiType}. Must be one of: ${Object.keys(BUILT_IN_DETECTORS).join(", ")}, or provide a custom detector.`);
161
162
  detector = BUILT_IN_DETECTORS[builtInType];
@@ -346,8 +347,9 @@ function piiMiddleware(piiType, options = {}) {
346
347
  strategy,
347
348
  detector
348
349
  });
350
+ const middlewareName = `PIIMiddleware[${resolvedRule.piiType}]`;
349
351
  return require_middleware.createMiddleware({
350
- name: `PIIMiddleware[${resolvedRule.piiType}]`,
352
+ name: middlewareName,
351
353
  contextSchema,
352
354
  beforeModel: async (state, runtime) => {
353
355
  const applyToInput = runtime.context.applyToInput ?? options.applyToInput ?? true;
@@ -1 +1 @@
1
- {"version":3,"file":"pii.cjs","names":["z","createMiddleware","HumanMessage","AIMessage","ToolMessage"],"sources":["../../../src/agents/middleware/pii.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { sha256 } from \"@langchain/core/utils/hash\";\nimport { AIMessage, HumanMessage, ToolMessage } from \"@langchain/core/messages\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\n/**\n * Represents a detected PII match in content\n */\nexport interface PIIMatch {\n /**\n * The matched text\n */\n text: string;\n /**\n * The start index of the match\n */\n start: number;\n /**\n * The end index of the match\n */\n end: number;\n}\n\n/**\n * Error thrown when PII is detected and strategy is 'block'\n */\nexport class PIIDetectionError extends Error {\n constructor(\n public readonly piiType: string,\n public readonly matches: PIIMatch[]\n ) {\n super(`PII detected: ${piiType} found ${matches.length} occurrence(s)`);\n this.name = \"PIIDetectionError\";\n }\n}\n\n/**\n * Strategy for handling detected PII\n */\nexport type PIIStrategy = \"block\" | \"redact\" | \"mask\" | \"hash\";\n\n/**\n * Built-in PII types\n */\nexport type BuiltInPIIType =\n | \"email\"\n | \"credit_card\"\n | \"ip\"\n | \"mac_address\"\n | \"url\";\n\n/**\n * Custom detector function that takes content and returns matches\n */\nexport type PIIDetector = (content: string) => PIIMatch[];\nexport type Detector = PIIDetector | RegExp | string;\n\n/**\n * Configuration for a redaction rule\n */\nexport interface RedactionRuleConfig {\n /**\n * Type of PII to detect (built-in or custom name)\n */\n piiType: BuiltInPIIType | string;\n /**\n * Strategy for handling detected PII\n */\n strategy: PIIStrategy;\n /**\n * Custom detector function or regex pattern string\n */\n detector?: Detector;\n}\n\n/**\n * Resolved redaction rule with a concrete detector function\n */\nexport interface ResolvedRedactionRule {\n piiType: string;\n strategy: PIIStrategy;\n detector: PIIDetector;\n}\n\n/**\n * Email detection regex pattern\n */\nconst EMAIL_PATTERN = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g;\n\n/**\n * Credit card detection regex pattern (basic, will be validated with Luhn)\n */\nconst CREDIT_CARD_PATTERN = /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g;\n\n/**\n * IP address detection regex pattern\n */\nconst IP_PATTERN =\n /\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b/g;\n\n/**\n * MAC address detection regex pattern\n */\nconst MAC_ADDRESS_PATTERN = /\\b(?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2})\\b/g;\n\n/**\n * URL detection regex pattern\n */\nconst URL_PATTERN = /(?:https?:\\/\\/|www\\.)[^\\s<>\"{}|\\\\^`[\\]]+/gi;\n\n/**\n * Luhn algorithm for credit card validation\n */\nfunction luhnCheck(cardNumber: string): boolean {\n const digits = cardNumber.replace(/\\D/g, \"\");\n let sum = 0;\n let isEven = false;\n\n for (let i = digits.length - 1; i >= 0; i--) {\n let digit = parseInt(digits[i], 10);\n\n if (isEven) {\n digit *= 2;\n if (digit > 9) {\n digit -= 9;\n }\n }\n\n sum += digit;\n isEven = !isEven;\n }\n\n return sum % 10 === 0;\n}\n\n/**\n * Convert regex match to PIIMatch\n */\nfunction regexMatchToPIIMatch(match: RegExpMatchArray): PIIMatch {\n return {\n text: match[0],\n start: match.index ?? 0,\n end: (match.index ?? 0) + match[0].length,\n };\n}\n\n/**\n * Detect email addresses in content\n */\nexport function detectEmail(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(EMAIL_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Detect credit card numbers in content (validated with Luhn algorithm)\n */\nexport function detectCreditCard(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(CREDIT_CARD_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const cardNumber = match[0].replace(/\\D/g, \"\");\n // Credit cards are typically 13-19 digits\n if (\n cardNumber.length >= 13 &&\n cardNumber.length <= 19 &&\n luhnCheck(cardNumber)\n ) {\n matches.push(regexMatchToPIIMatch(match));\n }\n }\n\n return matches;\n}\n\n/**\n * Detect IP addresses in content (validated)\n */\nexport function detectIP(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(IP_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const ip = match[0];\n // Additional validation: each octet should be 0-255\n const parts = ip.split(\".\");\n if (\n parts.length === 4 &&\n parts.every((part) => {\n const num = parseInt(part, 10);\n return num >= 0 && num <= 255;\n })\n ) {\n matches.push(regexMatchToPIIMatch(match));\n }\n }\n\n return matches;\n}\n\n/**\n * Detect MAC addresses in content\n */\nexport function detectMacAddress(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(MAC_ADDRESS_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Detect URLs in content\n */\nexport function detectUrl(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(URL_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Built-in detector map\n */\nconst BUILT_IN_DETECTORS: Record<BuiltInPIIType, PIIDetector> = {\n email: detectEmail,\n credit_card: detectCreditCard,\n ip: detectIP,\n mac_address: detectMacAddress,\n url: detectUrl,\n};\n\n/**\n * Resolve a redaction rule to a concrete detector function\n */\nexport function resolveRedactionRule(\n config: RedactionRuleConfig\n): ResolvedRedactionRule {\n let detector: PIIDetector;\n\n if (config.detector) {\n if (typeof config.detector === \"string\") {\n // Regex pattern string\n const regex = new RegExp(config.detector, \"g\");\n detector = (content: string) => {\n const matches: PIIMatch[] = [];\n let match: RegExpMatchArray | null;\n const regexCopy = new RegExp(regex);\n\n while ((match = regexCopy.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n };\n // oxlint-disable-next-line no-instanceof/no-instanceof\n } else if (config.detector instanceof RegExp) {\n detector = (content: string) => {\n // oxlint-disable-next-line no-instanceof/no-instanceof\n if (!(config.detector instanceof RegExp)) {\n throw new Error(\"Detector is required\");\n }\n const matches: PIIMatch[] = [];\n let match: RegExpMatchArray | null;\n while ((match = config.detector.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n };\n } else {\n detector = config.detector;\n }\n } else {\n // Use built-in detector\n const builtInType = config.piiType as BuiltInPIIType;\n if (!BUILT_IN_DETECTORS[builtInType]) {\n throw new Error(\n `Unknown PII type: ${config.piiType}. Must be one of: ${Object.keys(\n BUILT_IN_DETECTORS\n ).join(\", \")}, or provide a custom detector.`\n );\n }\n detector = BUILT_IN_DETECTORS[builtInType];\n }\n\n return {\n piiType: config.piiType,\n strategy: config.strategy,\n detector,\n };\n}\n\n/**\n * Apply redact strategy: replace with [REDACTED_TYPE]\n */\nfunction applyRedactStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const replacement = `[REDACTED_${piiType.toUpperCase()}]`;\n result =\n result.slice(0, match.start) + replacement + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply mask strategy: partially mask PII (show last few characters)\n */\nfunction applyMaskStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const text = match.text;\n let masked: string;\n\n if (piiType === \"credit_card\") {\n // Show last 4 digits: ****-****-****-1234\n const digits = text.replace(/\\D/g, \"\");\n const last4 = digits.slice(-4);\n masked = `****-****-****-${last4}`;\n } else if (piiType === \"email\") {\n // Show first char and domain: j***@example.com\n const [local, domain] = text.split(\"@\");\n if (local && domain) {\n masked = `${local[0]}***@${domain}`;\n } else {\n masked = \"***\";\n }\n } else {\n // Default: show last 4 characters\n const visibleChars = Math.min(4, text.length);\n masked = `${\"*\".repeat(\n Math.max(0, text.length - visibleChars)\n )}${text.slice(-visibleChars)}`;\n }\n\n result = result.slice(0, match.start) + masked + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply hash strategy: replace with deterministic hash\n */\nfunction applyHashStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const hash = sha256(match.text).slice(0, 8);\n const replacement = `<${piiType}_hash:${hash}>`;\n result =\n result.slice(0, match.start) + replacement + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply strategy to content based on matches\n */\nexport function applyStrategy(\n content: string,\n matches: PIIMatch[],\n strategy: PIIStrategy,\n piiType: string\n): string {\n if (matches.length === 0) {\n return content;\n }\n\n switch (strategy) {\n case \"block\":\n throw new PIIDetectionError(piiType, matches);\n case \"redact\":\n return applyRedactStrategy(content, matches, piiType);\n case \"mask\":\n return applyMaskStrategy(content, matches, piiType);\n case \"hash\":\n return applyHashStrategy(content, matches, piiType);\n default:\n throw new Error(`Unknown strategy: ${strategy}`);\n }\n}\n\n/**\n * Configuration schema for PII middleware\n */\nconst contextSchema = z.object({\n /**\n * Whether to check user messages before model call\n */\n applyToInput: z.boolean().optional(),\n /**\n * Whether to check AI messages after model call\n */\n applyToOutput: z.boolean().optional(),\n /**\n * Whether to check tool result messages after tool execution\n */\n applyToToolResults: z.boolean().optional(),\n});\n\nexport type PIIMiddlewareConfig = InferInteropZodInput<typeof contextSchema>;\n\n/**\n * Process content for PII detection and apply strategy\n */\nfunction processContent(\n content: string,\n rule: ResolvedRedactionRule\n): { content: string; matches: PIIMatch[] } {\n const matches = rule.detector(content);\n if (matches.length === 0) {\n return { content, matches: [] };\n }\n\n const sanitized = applyStrategy(\n content,\n matches,\n rule.strategy,\n rule.piiType\n );\n return { content: sanitized, matches };\n}\n\n/**\n * Creates a middleware that detects and handles personally identifiable information (PII)\n * in conversations.\n *\n * This middleware detects common PII types and applies configurable strategies to handle them.\n * It can detect emails, credit cards, IP addresses, MAC addresses, and URLs in both user input\n * and agent output.\n *\n * Built-in PII types:\n * - `email`: Email addresses\n * - `credit_card`: Credit card numbers (validated with Luhn algorithm)\n * - `ip`: IP addresses (validated)\n * - `mac_address`: MAC addresses\n * - `url`: URLs (both `http`/`https` and bare URLs)\n *\n * Strategies:\n * - `block`: Raise an exception when PII is detected\n * - `redact`: Replace PII with `[REDACTED_TYPE]` placeholders\n * - `mask`: Partially mask PII (e.g., `****-****-****-1234` for credit card)\n * - `hash`: Replace PII with deterministic hash (e.g., `<email_hash:a1b2c3d4>`)\n *\n * Strategy Selection Guide:\n * | Strategy | Preserves Identity? | Best For |\n * | -------- | ------------------- | --------------------------------------- |\n * | `block` | N/A | Avoid PII completely |\n * | `redact` | No | General compliance, log sanitization |\n * | `mask` | No | Human readability, customer service UIs |\n * | `hash` | Yes (pseudonymous) | Analytics, debugging |\n *\n * @param piiType - Type of PII to detect. Can be a built-in type (`email`, `credit_card`, `ip`, `mac_address`, `url`) or a custom type name.\n * @param options - Configuration options\n * @param options.strategy - How to handle detected PII. Defaults to `\"redact\"`.\n * @param options.detector - Custom detector function or regex pattern string. If not provided, uses built-in detector for the `piiType`.\n * @param options.applyToInput - Whether to check user messages before model call. Defaults to `true`.\n * @param options.applyToOutput - Whether to check AI messages after model call. Defaults to `false`.\n * @param options.applyToToolResults - Whether to check tool result messages after tool execution. Defaults to `false`.\n *\n * @returns Middleware instance for use with `createAgent`\n *\n * @throws {PIIDetectionError} When PII is detected and strategy is `'block'`\n * @throws {Error} If `piiType` is not built-in and no detector is provided\n *\n * @example Basic usage\n * ```typescript\n * import { piiMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * // Redact all emails in user input\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * middleware: [\n * piiMiddleware(\"email\", { strategy: \"redact\" }),\n * ],\n * });\n * ```\n *\n * @example Different strategies for different PII types\n * ```typescript\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * middleware: [\n * piiMiddleware(\"credit_card\", { strategy: \"mask\" }),\n * piiMiddleware(\"url\", { strategy: \"redact\" }),\n * piiMiddleware(\"ip\", { strategy: \"hash\" }),\n * ],\n * });\n * ```\n *\n * @example Custom PII type with regex\n * ```typescript\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * middleware: [\n * piiMiddleware(\"api_key\", {\n * detector: \"sk-[a-zA-Z0-9]{32}\",\n * strategy: \"block\",\n * }),\n * ],\n * });\n * ```\n *\n * @public\n */\nexport function piiMiddleware(\n piiType: BuiltInPIIType | string,\n options: {\n strategy?: PIIStrategy;\n detector?: Detector;\n applyToInput?: boolean;\n applyToOutput?: boolean;\n applyToToolResults?: boolean;\n } = {}\n) {\n const { strategy = \"redact\", detector } = options;\n const resolvedRule = resolveRedactionRule({\n piiType,\n strategy,\n detector,\n });\n\n const middlewareName = `PIIMiddleware[${resolvedRule.piiType}]`;\n\n return createMiddleware({\n name: middlewareName,\n contextSchema,\n beforeModel: async (state, runtime) => {\n const applyToInput =\n runtime.context.applyToInput ?? options.applyToInput ?? true;\n const applyToToolResults =\n runtime.context.applyToToolResults ??\n options.applyToToolResults ??\n false;\n\n if (!applyToInput && !applyToToolResults) {\n return;\n }\n\n const messages = state.messages;\n if (!messages || messages.length === 0) {\n return;\n }\n\n const newMessages = [...messages];\n let anyModified = false;\n\n // Check user input if enabled\n if (applyToInput) {\n // Get last user message\n let lastUserIdx: number | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (HumanMessage.isInstance(messages[i])) {\n lastUserIdx = i;\n break;\n }\n }\n\n if (lastUserIdx !== null) {\n const lastUserMsg = messages[lastUserIdx];\n if (lastUserMsg && lastUserMsg.content) {\n const content = String(lastUserMsg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length > 0) {\n newMessages[lastUserIdx] = new HumanMessage({\n content: newContent,\n id: lastUserMsg.id,\n name: lastUserMsg.name,\n });\n anyModified = true;\n }\n }\n }\n }\n\n // Check tool results if enabled\n if (applyToToolResults) {\n // Find the last AIMessage, then process all ToolMessage objects after it\n let lastAiIdx: number | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (AIMessage.isInstance(messages[i])) {\n lastAiIdx = i;\n break;\n }\n }\n\n if (lastAiIdx !== null) {\n // Get all tool messages after the last AI message\n for (let i = lastAiIdx + 1; i < messages.length; i++) {\n const msg = messages[i];\n if (ToolMessage.isInstance(msg)) {\n if (!msg.content) {\n continue;\n }\n\n const content = String(msg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length > 0) {\n newMessages[i] = new ToolMessage({\n content: newContent,\n id: msg.id,\n name: msg.name,\n tool_call_id: msg.tool_call_id,\n });\n anyModified = true;\n }\n }\n }\n }\n }\n\n if (anyModified) {\n return { messages: newMessages };\n }\n\n return;\n },\n afterModel: async (state, runtime) => {\n const applyToOutput =\n runtime.context.applyToOutput ?? options.applyToOutput ?? false;\n\n if (!applyToOutput) {\n return;\n }\n\n const messages = state.messages;\n if (!messages || messages.length === 0) {\n return;\n }\n\n // Get last AI message\n let lastAiIdx: number | null = null;\n let lastAiMsg: AIMessage | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n const msg = messages[i];\n if (AIMessage.isInstance(msg)) {\n lastAiMsg = msg;\n lastAiIdx = i;\n break;\n }\n }\n\n if (lastAiIdx === null || !lastAiMsg || !lastAiMsg.content) {\n return;\n }\n\n // Detect PII in message content\n const content = String(lastAiMsg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length === 0) {\n return;\n }\n\n // Create updated message\n const updatedMessage = new AIMessage({\n content: newContent,\n id: lastAiMsg.id,\n name: lastAiMsg.name,\n tool_calls: lastAiMsg.tool_calls,\n });\n\n // Return updated messages\n const newMessages = [...messages];\n newMessages[lastAiIdx] = updatedMessage;\n return { messages: newMessages };\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pii.cjs","names":["sha256","z","createMiddleware","HumanMessage","AIMessage","ToolMessage"],"sources":["../../../src/agents/middleware/pii.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { sha256 } from \"@langchain/core/utils/hash\";\nimport { AIMessage, HumanMessage, ToolMessage } from \"@langchain/core/messages\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\n/**\n * Represents a detected PII match in content\n */\nexport interface PIIMatch {\n /**\n * The matched text\n */\n text: string;\n /**\n * The start index of the match\n */\n start: number;\n /**\n * The end index of the match\n */\n end: number;\n}\n\n/**\n * Error thrown when PII is detected and strategy is 'block'\n */\nexport class PIIDetectionError extends Error {\n constructor(\n public readonly piiType: string,\n public readonly matches: PIIMatch[]\n ) {\n super(`PII detected: ${piiType} found ${matches.length} occurrence(s)`);\n this.name = \"PIIDetectionError\";\n }\n}\n\n/**\n * Strategy for handling detected PII\n */\nexport type PIIStrategy = \"block\" | \"redact\" | \"mask\" | \"hash\";\n\n/**\n * Built-in PII types\n */\nexport type BuiltInPIIType =\n | \"email\"\n | \"credit_card\"\n | \"ip\"\n | \"mac_address\"\n | \"url\";\n\n/**\n * Custom detector function that takes content and returns matches\n */\nexport type PIIDetector = (content: string) => PIIMatch[];\nexport type Detector = PIIDetector | RegExp | string;\n\n/**\n * Configuration for a redaction rule\n */\nexport interface RedactionRuleConfig {\n /**\n * Type of PII to detect (built-in or custom name)\n */\n piiType: BuiltInPIIType | string;\n /**\n * Strategy for handling detected PII\n */\n strategy: PIIStrategy;\n /**\n * Custom detector function or regex pattern string\n */\n detector?: Detector;\n}\n\n/**\n * Resolved redaction rule with a concrete detector function\n */\nexport interface ResolvedRedactionRule {\n piiType: string;\n strategy: PIIStrategy;\n detector: PIIDetector;\n}\n\n/**\n * Email detection regex pattern\n */\nconst EMAIL_PATTERN = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g;\n\n/**\n * Credit card detection regex pattern (basic, will be validated with Luhn)\n */\nconst CREDIT_CARD_PATTERN = /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g;\n\n/**\n * IP address detection regex pattern\n */\nconst IP_PATTERN =\n /\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b/g;\n\n/**\n * MAC address detection regex pattern\n */\nconst MAC_ADDRESS_PATTERN = /\\b(?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2})\\b/g;\n\n/**\n * URL detection regex pattern\n */\nconst URL_PATTERN = /(?:https?:\\/\\/|www\\.)[^\\s<>\"{}|\\\\^`[\\]]+/gi;\n\n/**\n * Luhn algorithm for credit card validation\n */\nfunction luhnCheck(cardNumber: string): boolean {\n const digits = cardNumber.replace(/\\D/g, \"\");\n let sum = 0;\n let isEven = false;\n\n for (let i = digits.length - 1; i >= 0; i--) {\n let digit = parseInt(digits[i], 10);\n\n if (isEven) {\n digit *= 2;\n if (digit > 9) {\n digit -= 9;\n }\n }\n\n sum += digit;\n isEven = !isEven;\n }\n\n return sum % 10 === 0;\n}\n\n/**\n * Convert regex match to PIIMatch\n */\nfunction regexMatchToPIIMatch(match: RegExpMatchArray): PIIMatch {\n return {\n text: match[0],\n start: match.index ?? 0,\n end: (match.index ?? 0) + match[0].length,\n };\n}\n\n/**\n * Detect email addresses in content\n */\nexport function detectEmail(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(EMAIL_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Detect credit card numbers in content (validated with Luhn algorithm)\n */\nexport function detectCreditCard(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(CREDIT_CARD_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const cardNumber = match[0].replace(/\\D/g, \"\");\n // Credit cards are typically 13-19 digits\n if (\n cardNumber.length >= 13 &&\n cardNumber.length <= 19 &&\n luhnCheck(cardNumber)\n ) {\n matches.push(regexMatchToPIIMatch(match));\n }\n }\n\n return matches;\n}\n\n/**\n * Detect IP addresses in content (validated)\n */\nexport function detectIP(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(IP_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const ip = match[0];\n // Additional validation: each octet should be 0-255\n const parts = ip.split(\".\");\n if (\n parts.length === 4 &&\n parts.every((part) => {\n const num = parseInt(part, 10);\n return num >= 0 && num <= 255;\n })\n ) {\n matches.push(regexMatchToPIIMatch(match));\n }\n }\n\n return matches;\n}\n\n/**\n * Detect MAC addresses in content\n */\nexport function detectMacAddress(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(MAC_ADDRESS_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Detect URLs in content\n */\nexport function detectUrl(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(URL_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Built-in detector map\n */\nconst BUILT_IN_DETECTORS: Record<BuiltInPIIType, PIIDetector> = {\n email: detectEmail,\n credit_card: detectCreditCard,\n ip: detectIP,\n mac_address: detectMacAddress,\n url: detectUrl,\n};\n\n/**\n * Resolve a redaction rule to a concrete detector function\n */\nexport function resolveRedactionRule(\n config: RedactionRuleConfig\n): ResolvedRedactionRule {\n let detector: PIIDetector;\n\n if (config.detector) {\n if (typeof config.detector === \"string\") {\n // Regex pattern string\n const regex = new RegExp(config.detector, \"g\");\n detector = (content: string) => {\n const matches: PIIMatch[] = [];\n let match: RegExpMatchArray | null;\n const regexCopy = new RegExp(regex);\n\n while ((match = regexCopy.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n };\n // oxlint-disable-next-line no-instanceof/no-instanceof\n } else if (config.detector instanceof RegExp) {\n detector = (content: string) => {\n // oxlint-disable-next-line no-instanceof/no-instanceof\n if (!(config.detector instanceof RegExp)) {\n throw new Error(\"Detector is required\");\n }\n const matches: PIIMatch[] = [];\n let match: RegExpMatchArray | null;\n while ((match = config.detector.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n };\n } else {\n detector = config.detector;\n }\n } else {\n // Use built-in detector\n const builtInType = config.piiType as BuiltInPIIType;\n if (!BUILT_IN_DETECTORS[builtInType]) {\n throw new Error(\n `Unknown PII type: ${config.piiType}. Must be one of: ${Object.keys(\n BUILT_IN_DETECTORS\n ).join(\", \")}, or provide a custom detector.`\n );\n }\n detector = BUILT_IN_DETECTORS[builtInType];\n }\n\n return {\n piiType: config.piiType,\n strategy: config.strategy,\n detector,\n };\n}\n\n/**\n * Apply redact strategy: replace with [REDACTED_TYPE]\n */\nfunction applyRedactStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const replacement = `[REDACTED_${piiType.toUpperCase()}]`;\n result =\n result.slice(0, match.start) + replacement + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply mask strategy: partially mask PII (show last few characters)\n */\nfunction applyMaskStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const text = match.text;\n let masked: string;\n\n if (piiType === \"credit_card\") {\n // Show last 4 digits: ****-****-****-1234\n const digits = text.replace(/\\D/g, \"\");\n const last4 = digits.slice(-4);\n masked = `****-****-****-${last4}`;\n } else if (piiType === \"email\") {\n // Show first char and domain: j***@example.com\n const [local, domain] = text.split(\"@\");\n if (local && domain) {\n masked = `${local[0]}***@${domain}`;\n } else {\n masked = \"***\";\n }\n } else {\n // Default: show last 4 characters\n const visibleChars = Math.min(4, text.length);\n masked = `${\"*\".repeat(\n Math.max(0, text.length - visibleChars)\n )}${text.slice(-visibleChars)}`;\n }\n\n result = result.slice(0, match.start) + masked + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply hash strategy: replace with deterministic hash\n */\nfunction applyHashStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const hash = sha256(match.text).slice(0, 8);\n const replacement = `<${piiType}_hash:${hash}>`;\n result =\n result.slice(0, match.start) + replacement + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply strategy to content based on matches\n */\nexport function applyStrategy(\n content: string,\n matches: PIIMatch[],\n strategy: PIIStrategy,\n piiType: string\n): string {\n if (matches.length === 0) {\n return content;\n }\n\n switch (strategy) {\n case \"block\":\n throw new PIIDetectionError(piiType, matches);\n case \"redact\":\n return applyRedactStrategy(content, matches, piiType);\n case \"mask\":\n return applyMaskStrategy(content, matches, piiType);\n case \"hash\":\n return applyHashStrategy(content, matches, piiType);\n default:\n throw new Error(`Unknown strategy: ${strategy}`);\n }\n}\n\n/**\n * Configuration schema for PII middleware\n */\nconst contextSchema = z.object({\n /**\n * Whether to check user messages before model call\n */\n applyToInput: z.boolean().optional(),\n /**\n * Whether to check AI messages after model call\n */\n applyToOutput: z.boolean().optional(),\n /**\n * Whether to check tool result messages after tool execution\n */\n applyToToolResults: z.boolean().optional(),\n});\n\nexport type PIIMiddlewareConfig = InferInteropZodInput<typeof contextSchema>;\n\n/**\n * Process content for PII detection and apply strategy\n */\nfunction processContent(\n content: string,\n rule: ResolvedRedactionRule\n): { content: string; matches: PIIMatch[] } {\n const matches = rule.detector(content);\n if (matches.length === 0) {\n return { content, matches: [] };\n }\n\n const sanitized = applyStrategy(\n content,\n matches,\n rule.strategy,\n rule.piiType\n );\n return { content: sanitized, matches };\n}\n\n/**\n * Creates a middleware that detects and handles personally identifiable information (PII)\n * in conversations.\n *\n * This middleware detects common PII types and applies configurable strategies to handle them.\n * It can detect emails, credit cards, IP addresses, MAC addresses, and URLs in both user input\n * and agent output.\n *\n * Built-in PII types:\n * - `email`: Email addresses\n * - `credit_card`: Credit card numbers (validated with Luhn algorithm)\n * - `ip`: IP addresses (validated)\n * - `mac_address`: MAC addresses\n * - `url`: URLs (both `http`/`https` and bare URLs)\n *\n * Strategies:\n * - `block`: Raise an exception when PII is detected\n * - `redact`: Replace PII with `[REDACTED_TYPE]` placeholders\n * - `mask`: Partially mask PII (e.g., `****-****-****-1234` for credit card)\n * - `hash`: Replace PII with deterministic hash (e.g., `<email_hash:a1b2c3d4>`)\n *\n * Strategy Selection Guide:\n * | Strategy | Preserves Identity? | Best For |\n * | -------- | ------------------- | --------------------------------------- |\n * | `block` | N/A | Avoid PII completely |\n * | `redact` | No | General compliance, log sanitization |\n * | `mask` | No | Human readability, customer service UIs |\n * | `hash` | Yes (pseudonymous) | Analytics, debugging |\n *\n * @param piiType - Type of PII to detect. Can be a built-in type (`email`, `credit_card`, `ip`, `mac_address`, `url`) or a custom type name.\n * @param options - Configuration options\n * @param options.strategy - How to handle detected PII. Defaults to `\"redact\"`.\n * @param options.detector - Custom detector function or regex pattern string. If not provided, uses built-in detector for the `piiType`.\n * @param options.applyToInput - Whether to check user messages before model call. Defaults to `true`.\n * @param options.applyToOutput - Whether to check AI messages after model call. Defaults to `false`.\n * @param options.applyToToolResults - Whether to check tool result messages after tool execution. Defaults to `false`.\n *\n * @returns Middleware instance for use with `createAgent`\n *\n * @throws {PIIDetectionError} When PII is detected and strategy is `'block'`\n * @throws {Error} If `piiType` is not built-in and no detector is provided\n *\n * @example Basic usage\n * ```typescript\n * import { piiMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * // Redact all emails in user input\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * middleware: [\n * piiMiddleware(\"email\", { strategy: \"redact\" }),\n * ],\n * });\n * ```\n *\n * @example Different strategies for different PII types\n * ```typescript\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * middleware: [\n * piiMiddleware(\"credit_card\", { strategy: \"mask\" }),\n * piiMiddleware(\"url\", { strategy: \"redact\" }),\n * piiMiddleware(\"ip\", { strategy: \"hash\" }),\n * ],\n * });\n * ```\n *\n * @example Custom PII type with regex\n * ```typescript\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * middleware: [\n * piiMiddleware(\"api_key\", {\n * detector: \"sk-[a-zA-Z0-9]{32}\",\n * strategy: \"block\",\n * }),\n * ],\n * });\n * ```\n *\n * @public\n */\nexport function piiMiddleware(\n piiType: BuiltInPIIType | string,\n options: {\n strategy?: PIIStrategy;\n detector?: Detector;\n applyToInput?: boolean;\n applyToOutput?: boolean;\n applyToToolResults?: boolean;\n } = {}\n) {\n const { strategy = \"redact\", detector } = options;\n const resolvedRule = resolveRedactionRule({\n piiType,\n strategy,\n detector,\n });\n\n const middlewareName = `PIIMiddleware[${resolvedRule.piiType}]`;\n\n return createMiddleware({\n name: middlewareName,\n contextSchema,\n beforeModel: async (state, runtime) => {\n const applyToInput =\n runtime.context.applyToInput ?? options.applyToInput ?? true;\n const applyToToolResults =\n runtime.context.applyToToolResults ??\n options.applyToToolResults ??\n false;\n\n if (!applyToInput && !applyToToolResults) {\n return;\n }\n\n const messages = state.messages;\n if (!messages || messages.length === 0) {\n return;\n }\n\n const newMessages = [...messages];\n let anyModified = false;\n\n // Check user input if enabled\n if (applyToInput) {\n // Get last user message\n let lastUserIdx: number | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (HumanMessage.isInstance(messages[i])) {\n lastUserIdx = i;\n break;\n }\n }\n\n if (lastUserIdx !== null) {\n const lastUserMsg = messages[lastUserIdx];\n if (lastUserMsg && lastUserMsg.content) {\n const content = String(lastUserMsg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length > 0) {\n newMessages[lastUserIdx] = new HumanMessage({\n content: newContent,\n id: lastUserMsg.id,\n name: lastUserMsg.name,\n });\n anyModified = true;\n }\n }\n }\n }\n\n // Check tool results if enabled\n if (applyToToolResults) {\n // Find the last AIMessage, then process all ToolMessage objects after it\n let lastAiIdx: number | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (AIMessage.isInstance(messages[i])) {\n lastAiIdx = i;\n break;\n }\n }\n\n if (lastAiIdx !== null) {\n // Get all tool messages after the last AI message\n for (let i = lastAiIdx + 1; i < messages.length; i++) {\n const msg = messages[i];\n if (ToolMessage.isInstance(msg)) {\n if (!msg.content) {\n continue;\n }\n\n const content = String(msg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length > 0) {\n newMessages[i] = new ToolMessage({\n content: newContent,\n id: msg.id,\n name: msg.name,\n tool_call_id: msg.tool_call_id,\n });\n anyModified = true;\n }\n }\n }\n }\n }\n\n if (anyModified) {\n return { messages: newMessages };\n }\n\n return;\n },\n afterModel: async (state, runtime) => {\n const applyToOutput =\n runtime.context.applyToOutput ?? options.applyToOutput ?? false;\n\n if (!applyToOutput) {\n return;\n }\n\n const messages = state.messages;\n if (!messages || messages.length === 0) {\n return;\n }\n\n // Get last AI message\n let lastAiIdx: number | null = null;\n let lastAiMsg: AIMessage | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n const msg = messages[i];\n if (AIMessage.isInstance(msg)) {\n lastAiMsg = msg;\n lastAiIdx = i;\n break;\n }\n }\n\n if (lastAiIdx === null || !lastAiMsg || !lastAiMsg.content) {\n return;\n }\n\n // Detect PII in message content\n const content = String(lastAiMsg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length === 0) {\n return;\n }\n\n // Create updated message\n const updatedMessage = new AIMessage({\n content: newContent,\n id: lastAiMsg.id,\n name: lastAiMsg.name,\n tool_calls: lastAiMsg.tool_calls,\n });\n\n // Return updated messages\n const newMessages = [...messages];\n newMessages[lastAiIdx] = updatedMessage;\n return { messages: newMessages };\n },\n });\n}\n"],"mappings":"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"}
@@ -138,24 +138,25 @@ const BUILT_IN_DETECTORS = {
138
138
  */
139
139
  function resolveRedactionRule(config) {
140
140
  let detector;
141
- if (config.detector) if (typeof config.detector === "string") {
142
- const regex = new RegExp(config.detector, "g");
143
- detector = (content) => {
141
+ if (config.detector) {
142
+ if (typeof config.detector === "string") {
143
+ const regex = new RegExp(config.detector, "g");
144
+ detector = (content) => {
145
+ const matches = [];
146
+ let match;
147
+ const regexCopy = new RegExp(regex);
148
+ while ((match = regexCopy.exec(content)) !== null) matches.push(regexMatchToPIIMatch(match));
149
+ return matches;
150
+ };
151
+ } else if (config.detector instanceof RegExp) detector = (content) => {
152
+ if (!(config.detector instanceof RegExp)) throw new Error("Detector is required");
144
153
  const matches = [];
145
154
  let match;
146
- const regexCopy = new RegExp(regex);
147
- while ((match = regexCopy.exec(content)) !== null) matches.push(regexMatchToPIIMatch(match));
155
+ while ((match = config.detector.exec(content)) !== null) matches.push(regexMatchToPIIMatch(match));
148
156
  return matches;
149
157
  };
150
- } else if (config.detector instanceof RegExp) detector = (content) => {
151
- if (!(config.detector instanceof RegExp)) throw new Error("Detector is required");
152
- const matches = [];
153
- let match;
154
- while ((match = config.detector.exec(content)) !== null) matches.push(regexMatchToPIIMatch(match));
155
- return matches;
156
- };
157
- else detector = config.detector;
158
- else {
158
+ else detector = config.detector;
159
+ } else {
159
160
  const builtInType = config.piiType;
160
161
  if (!BUILT_IN_DETECTORS[builtInType]) throw new Error(`Unknown PII type: ${config.piiType}. Must be one of: ${Object.keys(BUILT_IN_DETECTORS).join(", ")}, or provide a custom detector.`);
161
162
  detector = BUILT_IN_DETECTORS[builtInType];
@@ -346,8 +347,9 @@ function piiMiddleware(piiType, options = {}) {
346
347
  strategy,
347
348
  detector
348
349
  });
350
+ const middlewareName = `PIIMiddleware[${resolvedRule.piiType}]`;
349
351
  return createMiddleware({
350
- name: `PIIMiddleware[${resolvedRule.piiType}]`,
352
+ name: middlewareName,
351
353
  contextSchema,
352
354
  beforeModel: async (state, runtime) => {
353
355
  const applyToInput = runtime.context.applyToInput ?? options.applyToInput ?? true;