langchain 1.4.5-dev-1781048185730 → 1.4.6-dev-1781485641139
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.
- package/CHANGELOG.md +8 -0
- package/dist/agents/ReactAgent.cjs +5 -2
- package/dist/agents/ReactAgent.cjs.map +1 -1
- package/dist/agents/ReactAgent.d.cts +1 -1
- package/dist/agents/ReactAgent.d.cts.map +1 -1
- package/dist/agents/ReactAgent.d.ts +1 -1
- package/dist/agents/ReactAgent.d.ts.map +1 -1
- package/dist/agents/ReactAgent.js +4 -1
- package/dist/agents/ReactAgent.js.map +1 -1
- package/dist/agents/index.cjs +3 -1
- package/dist/agents/index.cjs.map +1 -1
- package/dist/agents/index.d.cts +3 -1
- package/dist/agents/index.d.cts.map +1 -1
- package/dist/agents/index.d.ts +3 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +3 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/middleware/constants.cjs +25 -0
- package/dist/agents/middleware/constants.cjs.map +1 -1
- package/dist/agents/middleware/constants.js +25 -0
- package/dist/agents/middleware/constants.js.map +1 -1
- package/dist/agents/middleware/contextEditing.cjs.map +1 -1
- package/dist/agents/middleware/contextEditing.d.cts.map +1 -1
- package/dist/agents/middleware/contextEditing.d.ts.map +1 -1
- package/dist/agents/middleware/contextEditing.js.map +1 -1
- package/dist/agents/middleware/hitl.cjs +68 -29
- package/dist/agents/middleware/hitl.cjs.map +1 -1
- package/dist/agents/middleware/hitl.d.cts +2 -105
- package/dist/agents/middleware/hitl.d.cts.map +1 -1
- package/dist/agents/middleware/hitl.d.ts +2 -105
- package/dist/agents/middleware/hitl.d.ts.map +1 -1
- package/dist/agents/middleware/hitl.js +68 -29
- package/dist/agents/middleware/hitl.js.map +1 -1
- package/dist/agents/middleware/index.cjs +1 -0
- package/dist/agents/middleware/index.d.cts +2 -1
- package/dist/agents/middleware/index.d.ts +2 -1
- package/dist/agents/middleware/index.js +1 -0
- package/dist/agents/middleware/llmToolSelector.cjs +14 -0
- package/dist/agents/middleware/llmToolSelector.cjs.map +1 -1
- package/dist/agents/middleware/llmToolSelector.js +14 -0
- package/dist/agents/middleware/llmToolSelector.js.map +1 -1
- package/dist/agents/middleware/modelCallLimit.cjs +12 -0
- package/dist/agents/middleware/modelCallLimit.cjs.map +1 -1
- package/dist/agents/middleware/modelCallLimit.js +12 -0
- package/dist/agents/middleware/modelCallLimit.js.map +1 -1
- package/dist/agents/middleware/modelRetry.cjs +10 -1
- package/dist/agents/middleware/modelRetry.cjs.map +1 -1
- package/dist/agents/middleware/modelRetry.js +10 -1
- package/dist/agents/middleware/modelRetry.js.map +1 -1
- package/dist/agents/middleware/pii.cjs +9 -0
- package/dist/agents/middleware/pii.cjs.map +1 -1
- package/dist/agents/middleware/pii.js +9 -0
- package/dist/agents/middleware/pii.js.map +1 -1
- package/dist/agents/middleware/piiRedaction.cjs +6 -1
- package/dist/agents/middleware/piiRedaction.cjs.map +1 -1
- package/dist/agents/middleware/piiRedaction.js +6 -1
- package/dist/agents/middleware/piiRedaction.js.map +1 -1
- package/dist/agents/middleware/provider/anthropic/promptCaching.cjs +19 -0
- package/dist/agents/middleware/provider/anthropic/promptCaching.cjs.map +1 -1
- package/dist/agents/middleware/provider/anthropic/promptCaching.js +19 -0
- package/dist/agents/middleware/provider/anthropic/promptCaching.js.map +1 -1
- package/dist/agents/middleware/provider/openai/moderation.cjs.map +1 -1
- package/dist/agents/middleware/provider/openai/moderation.js.map +1 -1
- package/dist/agents/middleware/providerToolSearch.cjs +130 -0
- package/dist/agents/middleware/providerToolSearch.cjs.map +1 -0
- package/dist/agents/middleware/providerToolSearch.d.cts +75 -0
- package/dist/agents/middleware/providerToolSearch.d.cts.map +1 -0
- package/dist/agents/middleware/providerToolSearch.d.ts +75 -0
- package/dist/agents/middleware/providerToolSearch.d.ts.map +1 -0
- package/dist/agents/middleware/providerToolSearch.js +129 -0
- package/dist/agents/middleware/providerToolSearch.js.map +1 -0
- package/dist/agents/middleware/summarization.cjs +62 -1
- package/dist/agents/middleware/summarization.cjs.map +1 -1
- package/dist/agents/middleware/summarization.js +62 -1
- package/dist/agents/middleware/summarization.js.map +1 -1
- package/dist/agents/middleware/todoListMiddleware.cjs.map +1 -1
- package/dist/agents/middleware/todoListMiddleware.js.map +1 -1
- package/dist/agents/middleware/toolCallLimit.cjs +24 -0
- package/dist/agents/middleware/toolCallLimit.cjs.map +1 -1
- package/dist/agents/middleware/toolCallLimit.js +24 -0
- package/dist/agents/middleware/toolCallLimit.js.map +1 -1
- package/dist/agents/middleware/toolEmulator.cjs.map +1 -1
- package/dist/agents/middleware/toolEmulator.js.map +1 -1
- package/dist/agents/middleware/toolRetry.cjs +17 -0
- package/dist/agents/middleware/toolRetry.cjs.map +1 -1
- package/dist/agents/middleware/toolRetry.js +17 -0
- package/dist/agents/middleware/toolRetry.js.map +1 -1
- package/dist/agents/middleware.cjs.map +1 -1
- package/dist/agents/middleware.js.map +1 -1
- package/dist/agents/nodes/AfterAgentNode.cjs.map +1 -1
- package/dist/agents/nodes/AfterAgentNode.js.map +1 -1
- package/dist/agents/nodes/AfterModelNode.cjs.map +1 -1
- package/dist/agents/nodes/AfterModelNode.js.map +1 -1
- package/dist/agents/nodes/AgentNode.cjs +17 -0
- package/dist/agents/nodes/AgentNode.cjs.map +1 -1
- package/dist/agents/nodes/AgentNode.js +17 -0
- package/dist/agents/nodes/AgentNode.js.map +1 -1
- package/dist/agents/nodes/BeforeAgentNode.cjs.map +1 -1
- package/dist/agents/nodes/BeforeAgentNode.js.map +1 -1
- package/dist/agents/nodes/BeforeModelNode.cjs.map +1 -1
- package/dist/agents/nodes/BeforeModelNode.js.map +1 -1
- package/dist/agents/nodes/ToolNode.cjs +3 -0
- package/dist/agents/nodes/ToolNode.cjs.map +1 -1
- package/dist/agents/nodes/ToolNode.js +3 -0
- package/dist/agents/nodes/ToolNode.js.map +1 -1
- package/dist/agents/nodes/middleware.cjs +3 -0
- package/dist/agents/nodes/middleware.cjs.map +1 -1
- package/dist/agents/nodes/middleware.js +3 -0
- package/dist/agents/nodes/middleware.js.map +1 -1
- package/dist/agents/nodes/utils.cjs.map +1 -1
- package/dist/agents/nodes/utils.js.map +1 -1
- package/dist/agents/responses.cjs.map +1 -1
- package/dist/agents/responses.js.map +1 -1
- package/dist/agents/tests/utils.cjs.map +1 -1
- package/dist/agents/tests/utils.js.map +1 -1
- package/dist/agents/transformers/index.cjs +2 -0
- package/dist/agents/transformers/index.d.cts +3 -0
- package/dist/agents/transformers/index.d.ts +3 -0
- package/dist/agents/transformers/index.js +3 -0
- package/dist/agents/transformers/subagent.cjs +205 -0
- package/dist/agents/transformers/subagent.cjs.map +1 -0
- package/dist/agents/transformers/subagent.d.cts +34 -0
- package/dist/agents/transformers/subagent.d.cts.map +1 -0
- package/dist/agents/transformers/subagent.d.ts +34 -0
- package/dist/agents/transformers/subagent.d.ts.map +1 -0
- package/dist/agents/transformers/subagent.js +204 -0
- package/dist/agents/transformers/subagent.js.map +1 -0
- package/dist/agents/{stream.cjs → transformers/tool-call.cjs} +3 -15
- package/dist/agents/transformers/tool-call.cjs.map +1 -0
- package/dist/agents/transformers/tool-call.d.cts +17 -0
- package/dist/agents/transformers/tool-call.d.cts.map +1 -0
- package/dist/agents/transformers/tool-call.d.ts +17 -0
- package/dist/agents/transformers/tool-call.d.ts.map +1 -0
- package/dist/agents/{stream.js → transformers/tool-call.js} +2 -14
- package/dist/agents/transformers/tool-call.js.map +1 -0
- package/dist/agents/{stream.d.cts → transformers/types.d.cts} +50 -20
- package/dist/agents/transformers/types.d.cts.map +1 -0
- package/dist/agents/{stream.d.ts → transformers/types.d.ts} +50 -20
- package/dist/agents/transformers/types.d.ts.map +1 -0
- package/dist/agents/utils.cjs +3 -0
- package/dist/agents/utils.cjs.map +1 -1
- package/dist/agents/utils.js +3 -0
- package/dist/agents/utils.js.map +1 -1
- package/dist/browser.cjs +9 -3
- package/dist/browser.d.cts +6 -3
- package/dist/browser.d.ts +6 -3
- package/dist/browser.js +6 -2
- package/dist/chat_models/universal.cjs.map +1 -1
- package/dist/chat_models/universal.js.map +1 -1
- package/dist/hub/index.cjs.map +1 -1
- package/dist/hub/index.js.map +1 -1
- package/dist/hub/node.cjs.map +1 -1
- package/dist/hub/node.js.map +1 -1
- package/dist/index.cjs +9 -3
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -2
- package/dist/storage/encoder_backed.cjs.map +1 -1
- package/dist/storage/encoder_backed.js.map +1 -1
- package/dist/storage/file_system.cjs.map +1 -1
- package/dist/storage/file_system.js.map +1 -1
- package/package.json +13 -13
- package/dist/agents/stream.cjs.map +0 -1
- package/dist/agents/stream.d.cts.map +0 -1
- package/dist/agents/stream.d.ts.map +0 -1
- package/dist/agents/stream.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llmToolSelector.cjs","names":["z","BaseLanguageModel","createMiddleware","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 { 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 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","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 { 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 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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"}
|
|
@@ -22,9 +22,23 @@ function createToolSelectionResponse(tools) {
|
|
|
22
22
|
* Options for configuring the LLM Tool Selector middleware.
|
|
23
23
|
*/
|
|
24
24
|
const LLMToolSelectorOptionsSchema = z.object({
|
|
25
|
+
/**
|
|
26
|
+
* The language model to use for tool selection (default: the provided model from the agent options).
|
|
27
|
+
*/
|
|
25
28
|
model: z.string().or(z.instanceof(BaseLanguageModel)).optional(),
|
|
29
|
+
/**
|
|
30
|
+
* System prompt for the tool selection model.
|
|
31
|
+
*/
|
|
26
32
|
systemPrompt: z.string().optional(),
|
|
33
|
+
/**
|
|
34
|
+
* Maximum number of tools to select. If the model selects more,
|
|
35
|
+
* only the first maxTools will be used. No limit if not specified.
|
|
36
|
+
*/
|
|
27
37
|
maxTools: z.number().optional(),
|
|
38
|
+
/**
|
|
39
|
+
* Tool names to always include regardless of selection.
|
|
40
|
+
* These do not count against the maxTools limit.
|
|
41
|
+
*/
|
|
28
42
|
alwaysInclude: z.array(z.string()).optional()
|
|
29
43
|
});
|
|
30
44
|
/**
|
|
@@ -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 { 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 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.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 { 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 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":";;;;;;;AAgBA,MAAM,wBACJ;;;;;;;AAmBF,SAAS,4BAA4B,OAAkC;AACrE,KAAI,CAAC,SAAS,MAAM,WAAW,EAC7B,OAAM,IAAI,MAAM,yCAAyC;CAI3D,MAAM,eAAe,MAAM,KAAK,SAAS,EAAE,QAAQ,KAAK,KAAK,CAAC;CAC9D,MAAM,WAAW,EAAE,MACjB,aAKD;AAED,QAAO,EAAE,OAAO,EACd,OAAO,EACJ,MAAM,SAAS,CACf,SAAS,qDAAqD,EAClE,CAAC;;;;;AAMJ,MAAa,+BAA+B,EAAE,OAAO;;;;CAInD,OAAO,EAAE,QAAQ,CAAC,GAAG,EAAE,WAAW,kBAAkB,CAAC,CAAC,UAAU;;;;CAIhE,cAAc,EAAE,QAAQ,CAAC,UAAU;;;;;CAKnC,UAAU,EAAE,QAAQ,CAAC,UAAU;;;;;CAK/B,eAAe,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU;CAC9C,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CF,SAAgB,0BAA0B,SAAgC;AACxE,QAAO,iBAAiB;EACtB,MAAM;EACN,eAAe;EACf,MAAM,cAAc,SAAS,SAAS;GACpC,MAAM,mBAAmB,MAAM,wBAC7B,SACA,SACA,QAAQ,QACT;AACD,OAAI,CAAC,iBACH,QAAO,QAAQ,QAAQ;GAIzB,MAAM,sBAAsB,4BAC1B,iBAAiB,eAClB;GACD,MAAM,kBACJ,MAAM,iBAAiB,MAAM,uBAC3B,oBACD;GAIH,MAAM,SAAS,aADb,uBAAuB,QAAQ,QAAQ,IAAI,EAAE,EACP;IACtC,UAAU,EAAE,WAAW,mBAAmB;IAC1C,WAAW,EAAE;IACd,CAAC;GAEF,MAAM,WAAW,MAAM,iBAAiB,OACtC,CACE;IAAE,MAAM;IAAU,SAAS,iBAAiB;IAAe,EAC3D,iBAAiB,gBAClB,EACD,OACD;AAGD,OAAI,CAAC,YAAY,OAAO,aAAa,YAAY,EAAE,WAAW,UAC5D,OAAM,IAAI,MACR,kDAAkD,OAAO,WAC1D;AAGH,UAAO,QACL,yBACE,UACA,iBAAiB,gBACjB,iBAAiB,gBACjB,SACA,QACD,CACF;;EAEJ,CAAC;;;;;;;;;;AAWJ,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,EAAE;CAC9D,MAAM,eACJ,QAAQ,QAAQ,gBAChB,QAAQ,gBACR;;;;AAKF,KAAI,CAAC,QAAQ,SAAS,QAAQ,MAAM,WAAW,EAC7C;;;;CAMF,MAAM,YAAY,QAAQ,MAAM,QAC7B,SACC,OAAO,SAAS,YAChB,UAAU,QACV,iBAAiB,QACjB,OAAO,KAAK,SAAS,SACxB;;;;AAKD,KAAI,cAAc,SAAS,GAAG;EAC5B,MAAM,qBAAqB,IAAI,IAAI,UAAU,KAAK,SAAS,KAAK,KAAK,CAAC;EACtE,MAAM,eAAe,cAAc,QAChC,SAAS,CAAC,mBAAmB,IAAI,KAAK,CACxC;AACD,MAAI,aAAa,SAAS,EACxB,OAAM,IAAI,MACR,gDAAgD,aAAa,KAC3D,KACD,CAAC,qBACoB,MAAM,KAAK,mBAAmB,CAAC,MAAM,CAAC,KAAK,KAAK,GACvE;;;;;CAOL,MAAM,iBAAiB,UAAU,QAC9B,SAAS,CAAC,cAAc,SAAS,KAAK,KAAK,CAC7C;;;;AAKD,KAAI,eAAe,WAAW,EAC5B;CAGF,IAAI,gBAAgB;;;;AAIpB,KAAI,aAAa,KAAA,EACf,kBACE;gJAGkB,SAAS;;;;CAM/B,IAAI;AACJ,MAAK,MAAM,WAAW,QAAQ,SAC5B,KAAI,aAAa,WAAW,QAAQ,CAClC,mBAAkB;AAItB,KAAI,CAAC,gBACH,OAAM,IAAI,MAAM,4CAA4C;CAG9D,MAAM,gBAAgB,CAAC,QAClB,QAAQ,QACT,OAAO,UAAU,WACf,MAAM,cAAc,MAAM,GAC1B;CAEN,MAAM,iBAAiB,eAAe,KAAK,SAAS,KAAK,KAAK;AAE9D,QAAO;EACL;EACA;EACA;EACA,OAAO;EACP;EACD;;;;;;;;;;;;AAaH,SAAS,yBAIP,UACA,gBACA,gBACA,SACA,SACgC;CAChC,MAAM,WAAW,QAAQ;CACzB,MAAM,gBAAgB,QAAQ,iBAAiB,EAAE;CAEjD,MAAM,oBAA8B,EAAE;CACtC,MAAM,wBAAkC,EAAE;AAE1C,MAAK,MAAM,YAAY,SAAS,OAAO;AACrC,MAAI,CAAC,eAAe,SAAS,SAAS,EAAE;AACtC,yBAAsB,KAAK,SAAS;AACpC;;;;;AAMF,MACE,CAAC,kBAAkB,SAAS,SAAS,KACpC,aAAa,KAAA,KAAa,kBAAkB,SAAS,UAEtD,mBAAkB,KAAK,SAAS;;AAIpC,KAAI,sBAAsB,SAAS,EACjC,OAAM,IAAI,MACR,iCAAiC,sBAAsB,KAAK,KAAK,GAClE;;;;CAMH,MAAM,gBAAgB,eAAe,QAAQ,SAC3C,kBAAkB,SAAS,KAAK,KAAK,CACtC;;;;CAKD,MAAM,uBAAuB,QAAQ,SAAS,EAAE,EAAE,QAC/C,SACC,OAAO,SAAS,YAChB,UAAU,QACV,OAAO,KAAK,SAAS,YACrB,cAAc,SAAS,KAAK,KAAK,CACpC;AACD,eAAc,KAAK,GAAG,oBAAoB;;;;CAK1C,MAAM,iBAAiB,QAAQ,SAAS,EAAE,EAAE,QACzC,SACC,EACE,OAAO,SAAS,YAChB,UAAU,QACV,iBAAiB,QACjB,OAAO,KAAK,SAAS,UAE1B;AAED,QAAO;EACL,GAAG;EACH,OAAO,CAAC,GAAG,eAAe,GAAG,cAAc;EAC5C"}
|
|
@@ -5,8 +5,20 @@ let zod_v3 = require("zod/v3");
|
|
|
5
5
|
//#region src/agents/middleware/modelCallLimit.ts
|
|
6
6
|
const DEFAULT_EXIT_BEHAVIOR = "end";
|
|
7
7
|
const contextSchema = zod_v3.z.object({
|
|
8
|
+
/**
|
|
9
|
+
* The maximum number of model calls allowed per thread.
|
|
10
|
+
*/
|
|
8
11
|
threadLimit: zod_v3.z.number().optional(),
|
|
12
|
+
/**
|
|
13
|
+
* The maximum number of model calls allowed per run.
|
|
14
|
+
*/
|
|
9
15
|
runLimit: zod_v3.z.number().optional(),
|
|
16
|
+
/**
|
|
17
|
+
* The behavior to take when the limit is exceeded.
|
|
18
|
+
* - "error" will throw an error and stop the agent.
|
|
19
|
+
* - "end" will end the agent.
|
|
20
|
+
* @default "end"
|
|
21
|
+
*/
|
|
10
22
|
exitBehavior: zod_v3.z.enum(["error", "end"]).optional()
|
|
11
23
|
});
|
|
12
24
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modelCallLimit.cjs","names":["z","createMiddleware","AIMessage"],"sources":["../../../src/agents/middleware/modelCallLimit.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { AIMessage } from \"@langchain/core/messages\";\nimport { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\nconst DEFAULT_EXIT_BEHAVIOR = \"end\";\n\nconst contextSchema = z.object({\n /**\n * The maximum number of model calls allowed per thread.\n */\n threadLimit: z.number().optional(),\n /**\n * The maximum number of model calls allowed per run.\n */\n runLimit: z.number().optional(),\n /**\n * The behavior to take when the limit is exceeded.\n * - \"error\" will throw an error and stop the agent.\n * - \"end\" will end the agent.\n * @default \"end\"\n */\n exitBehavior: z.enum([\"error\", \"end\"]).optional(),\n});\nexport type ModelCallLimitMiddlewareConfig = Partial<\n InferInteropZodInput<typeof contextSchema>\n>;\n\n/**\n * Middleware state schema to track the number of model calls made at the thread and run level.\n */\nconst stateSchema = z.object({\n threadModelCallCount: z.number().default(0),\n runModelCallCount: z.number().default(0),\n});\n\n/**\n * Error thrown when the model call limit is exceeded.\n *\n * @param threadLimit - The maximum number of model calls allowed per thread.\n * @param runLimit - The maximum number of model calls allowed per run.\n * @param threadCount - The number of model calls made at the thread level.\n * @param runCount - The number of model calls made at the run level.\n */\nclass ModelCallLimitMiddlewareError extends Error {\n constructor({\n threadLimit,\n runLimit,\n threadCount,\n runCount,\n }: {\n threadLimit?: number;\n runLimit?: number;\n threadCount?: number;\n runCount?: number;\n }) {\n const exceededHint: string[] = [];\n if (typeof threadLimit === \"number\" && typeof threadCount === \"number\") {\n exceededHint.push(\n `thread level call limit reached with ${threadCount} model calls`\n );\n }\n if (typeof runLimit === \"number\" && typeof runCount === \"number\") {\n exceededHint.push(\n `run level call limit reached with ${runCount} model calls`\n );\n }\n super(\n `Model call limits exceeded${\n exceededHint.length > 0 ? `: ${exceededHint.join(\", \")}` : \"\"\n }`\n );\n this.name = \"ModelCallLimitMiddlewareError\";\n }\n}\n\n/**\n * Creates a middleware to limit the number of model calls at both thread and run levels.\n *\n * This middleware helps prevent excessive model API calls by enforcing limits on how many\n * times the model can be invoked. It supports two types of limits:\n *\n * - **Thread-level limit**: Restricts the total number of model calls across an entire conversation thread\n * - **Run-level limit**: Restricts the number of model calls within a single agent run/invocation\n *\n * ## How It Works\n *\n * The middleware intercepts model requests before they are sent and checks the current call counts\n * against the configured limits. If either limit is exceeded, it throws a `ModelCallLimitMiddlewareError`\n * to stop execution and prevent further API calls.\n *\n * ## Use Cases\n *\n * - **Cost Control**: Prevent runaway costs from excessive model calls in production\n * - **Testing**: Ensure agents don't make too many calls during development/testing\n * - **Safety**: Limit potential infinite loops or recursive agent behaviors\n * - **Rate Limiting**: Enforce organizational policies on model usage per conversation\n *\n * @param middlewareOptions - Configuration options for the call limits\n * @param middlewareOptions.threadLimit - Maximum number of model calls allowed per thread (optional)\n * @param middlewareOptions.runLimit - Maximum number of model calls allowed per run (optional)\n *\n * @returns A middleware instance that can be passed to `createAgent`\n *\n * @throws {ModelCallLimitMiddlewareError} When either the thread or run limit is exceeded\n *\n * @example\n * ```typescript\n * import { createAgent, modelCallLimitMiddleware } from \"langchain\";\n *\n * // Limit to 10 calls per thread and 3 calls per run\n * const agent = createAgent({\n * model: \"openai:gpt-4o-mini\",\n * tools: [myTool],\n * middleware: [\n * modelCallLimitMiddleware({\n * threadLimit: 10,\n * runLimit: 3\n * })\n * ]\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Limits can also be configured at runtime via context\n * const result = await agent.invoke(\n * { messages: [\"Hello\"] },\n * {\n * configurable: {\n * threadLimit: 5 // Override the default limit for this run\n * }\n * }\n * );\n * ```\n */\nexport function modelCallLimitMiddleware(\n middlewareOptions?: ModelCallLimitMiddlewareConfig\n) {\n return createMiddleware({\n name: \"ModelCallLimitMiddleware\",\n contextSchema,\n stateSchema,\n beforeModel: {\n canJumpTo: [\"end\"],\n hook: (state, runtime) => {\n let exitBehavior =\n runtime.context.exitBehavior ??\n middlewareOptions?.exitBehavior ??\n DEFAULT_EXIT_BEHAVIOR;\n\n // @ts-expect-error - throw is deprecated\n if (exitBehavior === \"throw\") {\n console.warn(\n \"The 'throw' exit behavior is deprecated. Please use 'error' instead.\"\n );\n exitBehavior = \"error\";\n }\n\n const threadLimit =\n runtime.context.threadLimit ?? middlewareOptions?.threadLimit;\n const runLimit =\n runtime.context.runLimit ?? middlewareOptions?.runLimit;\n\n const threadCount = state.threadModelCallCount;\n const runCount = state.runModelCallCount;\n if (typeof threadLimit === \"number\" && threadLimit <= threadCount) {\n const error = new ModelCallLimitMiddlewareError({\n threadLimit,\n threadCount,\n });\n if (exitBehavior === \"end\") {\n return {\n jumpTo: \"end\",\n messages: [new AIMessage(error.message)],\n };\n }\n\n throw error;\n }\n if (typeof runLimit === \"number\" && runLimit <= runCount) {\n const error = new ModelCallLimitMiddlewareError({\n runLimit,\n runCount,\n });\n if (exitBehavior === \"end\") {\n return {\n jumpTo: \"end\",\n messages: [new AIMessage(error.message)],\n };\n }\n\n throw error;\n }\n\n return state;\n },\n },\n afterModel: (state) => ({\n runModelCallCount: state.runModelCallCount + 1,\n threadModelCallCount: state.threadModelCallCount + 1,\n }),\n afterAgent: () => ({\n runModelCallCount: 0,\n }),\n });\n}\n"],"mappings":";;;;;AAMA,MAAM,wBAAwB;AAE9B,MAAM,gBAAgBA,OAAAA,EAAE,OAAO
|
|
1
|
+
{"version":3,"file":"modelCallLimit.cjs","names":["z","createMiddleware","AIMessage"],"sources":["../../../src/agents/middleware/modelCallLimit.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { AIMessage } from \"@langchain/core/messages\";\nimport { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\nconst DEFAULT_EXIT_BEHAVIOR = \"end\";\n\nconst contextSchema = z.object({\n /**\n * The maximum number of model calls allowed per thread.\n */\n threadLimit: z.number().optional(),\n /**\n * The maximum number of model calls allowed per run.\n */\n runLimit: z.number().optional(),\n /**\n * The behavior to take when the limit is exceeded.\n * - \"error\" will throw an error and stop the agent.\n * - \"end\" will end the agent.\n * @default \"end\"\n */\n exitBehavior: z.enum([\"error\", \"end\"]).optional(),\n});\nexport type ModelCallLimitMiddlewareConfig = Partial<\n InferInteropZodInput<typeof contextSchema>\n>;\n\n/**\n * Middleware state schema to track the number of model calls made at the thread and run level.\n */\nconst stateSchema = z.object({\n threadModelCallCount: z.number().default(0),\n runModelCallCount: z.number().default(0),\n});\n\n/**\n * Error thrown when the model call limit is exceeded.\n *\n * @param threadLimit - The maximum number of model calls allowed per thread.\n * @param runLimit - The maximum number of model calls allowed per run.\n * @param threadCount - The number of model calls made at the thread level.\n * @param runCount - The number of model calls made at the run level.\n */\nclass ModelCallLimitMiddlewareError extends Error {\n constructor({\n threadLimit,\n runLimit,\n threadCount,\n runCount,\n }: {\n threadLimit?: number;\n runLimit?: number;\n threadCount?: number;\n runCount?: number;\n }) {\n const exceededHint: string[] = [];\n if (typeof threadLimit === \"number\" && typeof threadCount === \"number\") {\n exceededHint.push(\n `thread level call limit reached with ${threadCount} model calls`\n );\n }\n if (typeof runLimit === \"number\" && typeof runCount === \"number\") {\n exceededHint.push(\n `run level call limit reached with ${runCount} model calls`\n );\n }\n super(\n `Model call limits exceeded${\n exceededHint.length > 0 ? `: ${exceededHint.join(\", \")}` : \"\"\n }`\n );\n this.name = \"ModelCallLimitMiddlewareError\";\n }\n}\n\n/**\n * Creates a middleware to limit the number of model calls at both thread and run levels.\n *\n * This middleware helps prevent excessive model API calls by enforcing limits on how many\n * times the model can be invoked. It supports two types of limits:\n *\n * - **Thread-level limit**: Restricts the total number of model calls across an entire conversation thread\n * - **Run-level limit**: Restricts the number of model calls within a single agent run/invocation\n *\n * ## How It Works\n *\n * The middleware intercepts model requests before they are sent and checks the current call counts\n * against the configured limits. If either limit is exceeded, it throws a `ModelCallLimitMiddlewareError`\n * to stop execution and prevent further API calls.\n *\n * ## Use Cases\n *\n * - **Cost Control**: Prevent runaway costs from excessive model calls in production\n * - **Testing**: Ensure agents don't make too many calls during development/testing\n * - **Safety**: Limit potential infinite loops or recursive agent behaviors\n * - **Rate Limiting**: Enforce organizational policies on model usage per conversation\n *\n * @param middlewareOptions - Configuration options for the call limits\n * @param middlewareOptions.threadLimit - Maximum number of model calls allowed per thread (optional)\n * @param middlewareOptions.runLimit - Maximum number of model calls allowed per run (optional)\n *\n * @returns A middleware instance that can be passed to `createAgent`\n *\n * @throws {ModelCallLimitMiddlewareError} When either the thread or run limit is exceeded\n *\n * @example\n * ```typescript\n * import { createAgent, modelCallLimitMiddleware } from \"langchain\";\n *\n * // Limit to 10 calls per thread and 3 calls per run\n * const agent = createAgent({\n * model: \"openai:gpt-4o-mini\",\n * tools: [myTool],\n * middleware: [\n * modelCallLimitMiddleware({\n * threadLimit: 10,\n * runLimit: 3\n * })\n * ]\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Limits can also be configured at runtime via context\n * const result = await agent.invoke(\n * { messages: [\"Hello\"] },\n * {\n * configurable: {\n * threadLimit: 5 // Override the default limit for this run\n * }\n * }\n * );\n * ```\n */\nexport function modelCallLimitMiddleware(\n middlewareOptions?: ModelCallLimitMiddlewareConfig\n) {\n return createMiddleware({\n name: \"ModelCallLimitMiddleware\",\n contextSchema,\n stateSchema,\n beforeModel: {\n canJumpTo: [\"end\"],\n hook: (state, runtime) => {\n let exitBehavior =\n runtime.context.exitBehavior ??\n middlewareOptions?.exitBehavior ??\n DEFAULT_EXIT_BEHAVIOR;\n\n // @ts-expect-error - throw is deprecated\n if (exitBehavior === \"throw\") {\n console.warn(\n \"The 'throw' exit behavior is deprecated. Please use 'error' instead.\"\n );\n exitBehavior = \"error\";\n }\n\n const threadLimit =\n runtime.context.threadLimit ?? middlewareOptions?.threadLimit;\n const runLimit =\n runtime.context.runLimit ?? middlewareOptions?.runLimit;\n\n const threadCount = state.threadModelCallCount;\n const runCount = state.runModelCallCount;\n if (typeof threadLimit === \"number\" && threadLimit <= threadCount) {\n const error = new ModelCallLimitMiddlewareError({\n threadLimit,\n threadCount,\n });\n if (exitBehavior === \"end\") {\n return {\n jumpTo: \"end\",\n messages: [new AIMessage(error.message)],\n };\n }\n\n throw error;\n }\n if (typeof runLimit === \"number\" && runLimit <= runCount) {\n const error = new ModelCallLimitMiddlewareError({\n runLimit,\n runCount,\n });\n if (exitBehavior === \"end\") {\n return {\n jumpTo: \"end\",\n messages: [new AIMessage(error.message)],\n };\n }\n\n throw error;\n }\n\n return state;\n },\n },\n afterModel: (state) => ({\n runModelCallCount: state.runModelCallCount + 1,\n threadModelCallCount: state.threadModelCallCount + 1,\n }),\n afterAgent: () => ({\n runModelCallCount: 0,\n }),\n });\n}\n"],"mappings":";;;;;AAMA,MAAM,wBAAwB;AAE9B,MAAM,gBAAgBA,OAAAA,EAAE,OAAO;;;;CAI7B,aAAaA,OAAAA,EAAE,QAAQ,CAAC,UAAU;;;;CAIlC,UAAUA,OAAAA,EAAE,QAAQ,CAAC,UAAU;;;;;;;CAO/B,cAAcA,OAAAA,EAAE,KAAK,CAAC,SAAS,MAAM,CAAC,CAAC,UAAU;CAClD,CAAC;;;;AAQF,MAAM,cAAcA,OAAAA,EAAE,OAAO;CAC3B,sBAAsBA,OAAAA,EAAE,QAAQ,CAAC,QAAQ,EAAE;CAC3C,mBAAmBA,OAAAA,EAAE,QAAQ,CAAC,QAAQ,EAAE;CACzC,CAAC;;;;;;;;;AAUF,IAAM,gCAAN,cAA4C,MAAM;CAChD,YAAY,EACV,aACA,UACA,aACA,YAMC;EACD,MAAM,eAAyB,EAAE;AACjC,MAAI,OAAO,gBAAgB,YAAY,OAAO,gBAAgB,SAC5D,cAAa,KACX,wCAAwC,YAAY,cACrD;AAEH,MAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SACtD,cAAa,KACX,qCAAqC,SAAS,cAC/C;AAEH,QACE,6BACE,aAAa,SAAS,IAAI,KAAK,aAAa,KAAK,KAAK,KAAK,KAE9D;AACD,OAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEhB,SAAgB,yBACd,mBACA;AACA,QAAOC,mBAAAA,iBAAiB;EACtB,MAAM;EACN;EACA;EACA,aAAa;GACX,WAAW,CAAC,MAAM;GAClB,OAAO,OAAO,YAAY;IACxB,IAAI,eACF,QAAQ,QAAQ,gBAChB,mBAAmB,gBACnB;AAGF,QAAI,iBAAiB,SAAS;AAC5B,aAAQ,KACN,uEACD;AACD,oBAAe;;IAGjB,MAAM,cACJ,QAAQ,QAAQ,eAAe,mBAAmB;IACpD,MAAM,WACJ,QAAQ,QAAQ,YAAY,mBAAmB;IAEjD,MAAM,cAAc,MAAM;IAC1B,MAAM,WAAW,MAAM;AACvB,QAAI,OAAO,gBAAgB,YAAY,eAAe,aAAa;KACjE,MAAM,QAAQ,IAAI,8BAA8B;MAC9C;MACA;MACD,CAAC;AACF,SAAI,iBAAiB,MACnB,QAAO;MACL,QAAQ;MACR,UAAU,CAAC,IAAIC,yBAAAA,UAAU,MAAM,QAAQ,CAAC;MACzC;AAGH,WAAM;;AAER,QAAI,OAAO,aAAa,YAAY,YAAY,UAAU;KACxD,MAAM,QAAQ,IAAI,8BAA8B;MAC9C;MACA;MACD,CAAC;AACF,SAAI,iBAAiB,MACnB,QAAO;MACL,QAAQ;MACR,UAAU,CAAC,IAAIA,yBAAAA,UAAU,MAAM,QAAQ,CAAC;MACzC;AAGH,WAAM;;AAGR,WAAO;;GAEV;EACD,aAAa,WAAW;GACtB,mBAAmB,MAAM,oBAAoB;GAC7C,sBAAsB,MAAM,uBAAuB;GACpD;EACD,mBAAmB,EACjB,mBAAmB,GACpB;EACF,CAAC"}
|
|
@@ -4,8 +4,20 @@ import { z } from "zod/v3";
|
|
|
4
4
|
//#region src/agents/middleware/modelCallLimit.ts
|
|
5
5
|
const DEFAULT_EXIT_BEHAVIOR = "end";
|
|
6
6
|
const contextSchema = z.object({
|
|
7
|
+
/**
|
|
8
|
+
* The maximum number of model calls allowed per thread.
|
|
9
|
+
*/
|
|
7
10
|
threadLimit: z.number().optional(),
|
|
11
|
+
/**
|
|
12
|
+
* The maximum number of model calls allowed per run.
|
|
13
|
+
*/
|
|
8
14
|
runLimit: z.number().optional(),
|
|
15
|
+
/**
|
|
16
|
+
* The behavior to take when the limit is exceeded.
|
|
17
|
+
* - "error" will throw an error and stop the agent.
|
|
18
|
+
* - "end" will end the agent.
|
|
19
|
+
* @default "end"
|
|
20
|
+
*/
|
|
9
21
|
exitBehavior: z.enum(["error", "end"]).optional()
|
|
10
22
|
});
|
|
11
23
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modelCallLimit.js","names":[],"sources":["../../../src/agents/middleware/modelCallLimit.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { AIMessage } from \"@langchain/core/messages\";\nimport { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\nconst DEFAULT_EXIT_BEHAVIOR = \"end\";\n\nconst contextSchema = z.object({\n /**\n * The maximum number of model calls allowed per thread.\n */\n threadLimit: z.number().optional(),\n /**\n * The maximum number of model calls allowed per run.\n */\n runLimit: z.number().optional(),\n /**\n * The behavior to take when the limit is exceeded.\n * - \"error\" will throw an error and stop the agent.\n * - \"end\" will end the agent.\n * @default \"end\"\n */\n exitBehavior: z.enum([\"error\", \"end\"]).optional(),\n});\nexport type ModelCallLimitMiddlewareConfig = Partial<\n InferInteropZodInput<typeof contextSchema>\n>;\n\n/**\n * Middleware state schema to track the number of model calls made at the thread and run level.\n */\nconst stateSchema = z.object({\n threadModelCallCount: z.number().default(0),\n runModelCallCount: z.number().default(0),\n});\n\n/**\n * Error thrown when the model call limit is exceeded.\n *\n * @param threadLimit - The maximum number of model calls allowed per thread.\n * @param runLimit - The maximum number of model calls allowed per run.\n * @param threadCount - The number of model calls made at the thread level.\n * @param runCount - The number of model calls made at the run level.\n */\nclass ModelCallLimitMiddlewareError extends Error {\n constructor({\n threadLimit,\n runLimit,\n threadCount,\n runCount,\n }: {\n threadLimit?: number;\n runLimit?: number;\n threadCount?: number;\n runCount?: number;\n }) {\n const exceededHint: string[] = [];\n if (typeof threadLimit === \"number\" && typeof threadCount === \"number\") {\n exceededHint.push(\n `thread level call limit reached with ${threadCount} model calls`\n );\n }\n if (typeof runLimit === \"number\" && typeof runCount === \"number\") {\n exceededHint.push(\n `run level call limit reached with ${runCount} model calls`\n );\n }\n super(\n `Model call limits exceeded${\n exceededHint.length > 0 ? `: ${exceededHint.join(\", \")}` : \"\"\n }`\n );\n this.name = \"ModelCallLimitMiddlewareError\";\n }\n}\n\n/**\n * Creates a middleware to limit the number of model calls at both thread and run levels.\n *\n * This middleware helps prevent excessive model API calls by enforcing limits on how many\n * times the model can be invoked. It supports two types of limits:\n *\n * - **Thread-level limit**: Restricts the total number of model calls across an entire conversation thread\n * - **Run-level limit**: Restricts the number of model calls within a single agent run/invocation\n *\n * ## How It Works\n *\n * The middleware intercepts model requests before they are sent and checks the current call counts\n * against the configured limits. If either limit is exceeded, it throws a `ModelCallLimitMiddlewareError`\n * to stop execution and prevent further API calls.\n *\n * ## Use Cases\n *\n * - **Cost Control**: Prevent runaway costs from excessive model calls in production\n * - **Testing**: Ensure agents don't make too many calls during development/testing\n * - **Safety**: Limit potential infinite loops or recursive agent behaviors\n * - **Rate Limiting**: Enforce organizational policies on model usage per conversation\n *\n * @param middlewareOptions - Configuration options for the call limits\n * @param middlewareOptions.threadLimit - Maximum number of model calls allowed per thread (optional)\n * @param middlewareOptions.runLimit - Maximum number of model calls allowed per run (optional)\n *\n * @returns A middleware instance that can be passed to `createAgent`\n *\n * @throws {ModelCallLimitMiddlewareError} When either the thread or run limit is exceeded\n *\n * @example\n * ```typescript\n * import { createAgent, modelCallLimitMiddleware } from \"langchain\";\n *\n * // Limit to 10 calls per thread and 3 calls per run\n * const agent = createAgent({\n * model: \"openai:gpt-4o-mini\",\n * tools: [myTool],\n * middleware: [\n * modelCallLimitMiddleware({\n * threadLimit: 10,\n * runLimit: 3\n * })\n * ]\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Limits can also be configured at runtime via context\n * const result = await agent.invoke(\n * { messages: [\"Hello\"] },\n * {\n * configurable: {\n * threadLimit: 5 // Override the default limit for this run\n * }\n * }\n * );\n * ```\n */\nexport function modelCallLimitMiddleware(\n middlewareOptions?: ModelCallLimitMiddlewareConfig\n) {\n return createMiddleware({\n name: \"ModelCallLimitMiddleware\",\n contextSchema,\n stateSchema,\n beforeModel: {\n canJumpTo: [\"end\"],\n hook: (state, runtime) => {\n let exitBehavior =\n runtime.context.exitBehavior ??\n middlewareOptions?.exitBehavior ??\n DEFAULT_EXIT_BEHAVIOR;\n\n // @ts-expect-error - throw is deprecated\n if (exitBehavior === \"throw\") {\n console.warn(\n \"The 'throw' exit behavior is deprecated. Please use 'error' instead.\"\n );\n exitBehavior = \"error\";\n }\n\n const threadLimit =\n runtime.context.threadLimit ?? middlewareOptions?.threadLimit;\n const runLimit =\n runtime.context.runLimit ?? middlewareOptions?.runLimit;\n\n const threadCount = state.threadModelCallCount;\n const runCount = state.runModelCallCount;\n if (typeof threadLimit === \"number\" && threadLimit <= threadCount) {\n const error = new ModelCallLimitMiddlewareError({\n threadLimit,\n threadCount,\n });\n if (exitBehavior === \"end\") {\n return {\n jumpTo: \"end\",\n messages: [new AIMessage(error.message)],\n };\n }\n\n throw error;\n }\n if (typeof runLimit === \"number\" && runLimit <= runCount) {\n const error = new ModelCallLimitMiddlewareError({\n runLimit,\n runCount,\n });\n if (exitBehavior === \"end\") {\n return {\n jumpTo: \"end\",\n messages: [new AIMessage(error.message)],\n };\n }\n\n throw error;\n }\n\n return state;\n },\n },\n afterModel: (state) => ({\n runModelCallCount: state.runModelCallCount + 1,\n threadModelCallCount: state.threadModelCallCount + 1,\n }),\n afterAgent: () => ({\n runModelCallCount: 0,\n }),\n });\n}\n"],"mappings":";;;;AAMA,MAAM,wBAAwB;AAE9B,MAAM,gBAAgB,EAAE,OAAO
|
|
1
|
+
{"version":3,"file":"modelCallLimit.js","names":[],"sources":["../../../src/agents/middleware/modelCallLimit.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { AIMessage } from \"@langchain/core/messages\";\nimport { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\nconst DEFAULT_EXIT_BEHAVIOR = \"end\";\n\nconst contextSchema = z.object({\n /**\n * The maximum number of model calls allowed per thread.\n */\n threadLimit: z.number().optional(),\n /**\n * The maximum number of model calls allowed per run.\n */\n runLimit: z.number().optional(),\n /**\n * The behavior to take when the limit is exceeded.\n * - \"error\" will throw an error and stop the agent.\n * - \"end\" will end the agent.\n * @default \"end\"\n */\n exitBehavior: z.enum([\"error\", \"end\"]).optional(),\n});\nexport type ModelCallLimitMiddlewareConfig = Partial<\n InferInteropZodInput<typeof contextSchema>\n>;\n\n/**\n * Middleware state schema to track the number of model calls made at the thread and run level.\n */\nconst stateSchema = z.object({\n threadModelCallCount: z.number().default(0),\n runModelCallCount: z.number().default(0),\n});\n\n/**\n * Error thrown when the model call limit is exceeded.\n *\n * @param threadLimit - The maximum number of model calls allowed per thread.\n * @param runLimit - The maximum number of model calls allowed per run.\n * @param threadCount - The number of model calls made at the thread level.\n * @param runCount - The number of model calls made at the run level.\n */\nclass ModelCallLimitMiddlewareError extends Error {\n constructor({\n threadLimit,\n runLimit,\n threadCount,\n runCount,\n }: {\n threadLimit?: number;\n runLimit?: number;\n threadCount?: number;\n runCount?: number;\n }) {\n const exceededHint: string[] = [];\n if (typeof threadLimit === \"number\" && typeof threadCount === \"number\") {\n exceededHint.push(\n `thread level call limit reached with ${threadCount} model calls`\n );\n }\n if (typeof runLimit === \"number\" && typeof runCount === \"number\") {\n exceededHint.push(\n `run level call limit reached with ${runCount} model calls`\n );\n }\n super(\n `Model call limits exceeded${\n exceededHint.length > 0 ? `: ${exceededHint.join(\", \")}` : \"\"\n }`\n );\n this.name = \"ModelCallLimitMiddlewareError\";\n }\n}\n\n/**\n * Creates a middleware to limit the number of model calls at both thread and run levels.\n *\n * This middleware helps prevent excessive model API calls by enforcing limits on how many\n * times the model can be invoked. It supports two types of limits:\n *\n * - **Thread-level limit**: Restricts the total number of model calls across an entire conversation thread\n * - **Run-level limit**: Restricts the number of model calls within a single agent run/invocation\n *\n * ## How It Works\n *\n * The middleware intercepts model requests before they are sent and checks the current call counts\n * against the configured limits. If either limit is exceeded, it throws a `ModelCallLimitMiddlewareError`\n * to stop execution and prevent further API calls.\n *\n * ## Use Cases\n *\n * - **Cost Control**: Prevent runaway costs from excessive model calls in production\n * - **Testing**: Ensure agents don't make too many calls during development/testing\n * - **Safety**: Limit potential infinite loops or recursive agent behaviors\n * - **Rate Limiting**: Enforce organizational policies on model usage per conversation\n *\n * @param middlewareOptions - Configuration options for the call limits\n * @param middlewareOptions.threadLimit - Maximum number of model calls allowed per thread (optional)\n * @param middlewareOptions.runLimit - Maximum number of model calls allowed per run (optional)\n *\n * @returns A middleware instance that can be passed to `createAgent`\n *\n * @throws {ModelCallLimitMiddlewareError} When either the thread or run limit is exceeded\n *\n * @example\n * ```typescript\n * import { createAgent, modelCallLimitMiddleware } from \"langchain\";\n *\n * // Limit to 10 calls per thread and 3 calls per run\n * const agent = createAgent({\n * model: \"openai:gpt-4o-mini\",\n * tools: [myTool],\n * middleware: [\n * modelCallLimitMiddleware({\n * threadLimit: 10,\n * runLimit: 3\n * })\n * ]\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Limits can also be configured at runtime via context\n * const result = await agent.invoke(\n * { messages: [\"Hello\"] },\n * {\n * configurable: {\n * threadLimit: 5 // Override the default limit for this run\n * }\n * }\n * );\n * ```\n */\nexport function modelCallLimitMiddleware(\n middlewareOptions?: ModelCallLimitMiddlewareConfig\n) {\n return createMiddleware({\n name: \"ModelCallLimitMiddleware\",\n contextSchema,\n stateSchema,\n beforeModel: {\n canJumpTo: [\"end\"],\n hook: (state, runtime) => {\n let exitBehavior =\n runtime.context.exitBehavior ??\n middlewareOptions?.exitBehavior ??\n DEFAULT_EXIT_BEHAVIOR;\n\n // @ts-expect-error - throw is deprecated\n if (exitBehavior === \"throw\") {\n console.warn(\n \"The 'throw' exit behavior is deprecated. Please use 'error' instead.\"\n );\n exitBehavior = \"error\";\n }\n\n const threadLimit =\n runtime.context.threadLimit ?? middlewareOptions?.threadLimit;\n const runLimit =\n runtime.context.runLimit ?? middlewareOptions?.runLimit;\n\n const threadCount = state.threadModelCallCount;\n const runCount = state.runModelCallCount;\n if (typeof threadLimit === \"number\" && threadLimit <= threadCount) {\n const error = new ModelCallLimitMiddlewareError({\n threadLimit,\n threadCount,\n });\n if (exitBehavior === \"end\") {\n return {\n jumpTo: \"end\",\n messages: [new AIMessage(error.message)],\n };\n }\n\n throw error;\n }\n if (typeof runLimit === \"number\" && runLimit <= runCount) {\n const error = new ModelCallLimitMiddlewareError({\n runLimit,\n runCount,\n });\n if (exitBehavior === \"end\") {\n return {\n jumpTo: \"end\",\n messages: [new AIMessage(error.message)],\n };\n }\n\n throw error;\n }\n\n return state;\n },\n },\n afterModel: (state) => ({\n runModelCallCount: state.runModelCallCount + 1,\n threadModelCallCount: state.threadModelCallCount + 1,\n }),\n afterAgent: () => ({\n runModelCallCount: 0,\n }),\n });\n}\n"],"mappings":";;;;AAMA,MAAM,wBAAwB;AAE9B,MAAM,gBAAgB,EAAE,OAAO;;;;CAI7B,aAAa,EAAE,QAAQ,CAAC,UAAU;;;;CAIlC,UAAU,EAAE,QAAQ,CAAC,UAAU;;;;;;;CAO/B,cAAc,EAAE,KAAK,CAAC,SAAS,MAAM,CAAC,CAAC,UAAU;CAClD,CAAC;;;;AAQF,MAAM,cAAc,EAAE,OAAO;CAC3B,sBAAsB,EAAE,QAAQ,CAAC,QAAQ,EAAE;CAC3C,mBAAmB,EAAE,QAAQ,CAAC,QAAQ,EAAE;CACzC,CAAC;;;;;;;;;AAUF,IAAM,gCAAN,cAA4C,MAAM;CAChD,YAAY,EACV,aACA,UACA,aACA,YAMC;EACD,MAAM,eAAyB,EAAE;AACjC,MAAI,OAAO,gBAAgB,YAAY,OAAO,gBAAgB,SAC5D,cAAa,KACX,wCAAwC,YAAY,cACrD;AAEH,MAAI,OAAO,aAAa,YAAY,OAAO,aAAa,SACtD,cAAa,KACX,qCAAqC,SAAS,cAC/C;AAEH,QACE,6BACE,aAAa,SAAS,IAAI,KAAK,aAAa,KAAK,KAAK,KAAK,KAE9D;AACD,OAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEhB,SAAgB,yBACd,mBACA;AACA,QAAO,iBAAiB;EACtB,MAAM;EACN;EACA;EACA,aAAa;GACX,WAAW,CAAC,MAAM;GAClB,OAAO,OAAO,YAAY;IACxB,IAAI,eACF,QAAQ,QAAQ,gBAChB,mBAAmB,gBACnB;AAGF,QAAI,iBAAiB,SAAS;AAC5B,aAAQ,KACN,uEACD;AACD,oBAAe;;IAGjB,MAAM,cACJ,QAAQ,QAAQ,eAAe,mBAAmB;IACpD,MAAM,WACJ,QAAQ,QAAQ,YAAY,mBAAmB;IAEjD,MAAM,cAAc,MAAM;IAC1B,MAAM,WAAW,MAAM;AACvB,QAAI,OAAO,gBAAgB,YAAY,eAAe,aAAa;KACjE,MAAM,QAAQ,IAAI,8BAA8B;MAC9C;MACA;MACD,CAAC;AACF,SAAI,iBAAiB,MACnB,QAAO;MACL,QAAQ;MACR,UAAU,CAAC,IAAI,UAAU,MAAM,QAAQ,CAAC;MACzC;AAGH,WAAM;;AAER,QAAI,OAAO,aAAa,YAAY,YAAY,UAAU;KACxD,MAAM,QAAQ,IAAI,8BAA8B;MAC9C;MACA;MACD,CAAC;AACF,SAAI,iBAAiB,MACnB,QAAO;MACL,QAAQ;MACR,UAAU,CAAC,IAAI,UAAU,MAAM,QAAQ,CAAC;MACzC;AAGH,WAAM;;AAGR,WAAO;;GAEV;EACD,aAAa,WAAW;GACtB,mBAAmB,MAAM,oBAAoB;GAC7C,sBAAsB,MAAM,uBAAuB;GACpD;EACD,mBAAmB,EACjB,mBAAmB,GACpB;EACF,CAAC"}
|
|
@@ -12,7 +12,16 @@ let zod_v3 = require("zod/v3");
|
|
|
12
12
|
/**
|
|
13
13
|
* Configuration options for the Model Retry Middleware.
|
|
14
14
|
*/
|
|
15
|
-
const ModelRetryMiddlewareOptionsSchema = zod_v3.z.object({
|
|
15
|
+
const ModelRetryMiddlewareOptionsSchema = zod_v3.z.object({
|
|
16
|
+
/**
|
|
17
|
+
* Behavior when all retries are exhausted. Options:
|
|
18
|
+
* - `"continue"` (default): Return an AIMessage with error details, allowing
|
|
19
|
+
* the agent to potentially handle the failure gracefully.
|
|
20
|
+
* - `"error"`: Re-raise the exception, stopping agent execution.
|
|
21
|
+
* - Custom function: Function that takes the exception and returns a string
|
|
22
|
+
* for the AIMessage content, allowing custom error formatting.
|
|
23
|
+
*/
|
|
24
|
+
onFailure: zod_v3.z.union([
|
|
16
25
|
zod_v3.z.literal("error"),
|
|
17
26
|
zod_v3.z.literal("continue"),
|
|
18
27
|
zod_v3.z.function().args(zod_v3.z.instanceof(Error)).returns(zod_v3.z.string())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modelRetry.cjs","names":["z","RetrySchema","InvalidRetryConfigError","AIMessage","createMiddleware","calculateRetryDelay","sleep"],"sources":["../../../src/agents/middleware/modelRetry.ts"],"sourcesContent":["/**\n * Model retry middleware for agents.\n */\nimport { z } from \"zod/v3\";\nimport { AIMessage } from \"@langchain/core/messages\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { sleep, calculateRetryDelay } from \"./utils.js\";\nimport { RetrySchema } from \"./constants.js\";\nimport { InvalidRetryConfigError } from \"./error.js\";\n\n/**\n * Configuration options for the Model Retry Middleware.\n */\nexport const ModelRetryMiddlewareOptionsSchema = z\n .object({\n /**\n * Behavior when all retries are exhausted. Options:\n * - `\"continue\"` (default): Return an AIMessage with error details, allowing\n * the agent to potentially handle the failure gracefully.\n * - `\"error\"`: Re-raise the exception, stopping agent execution.\n * - Custom function: Function that takes the exception and returns a string\n * for the AIMessage content, allowing custom error formatting.\n */\n onFailure: z\n .union([\n z.literal(\"error\"),\n z.literal(\"continue\"),\n z.function().args(z.instanceof(Error)).returns(z.string()),\n ])\n .default(\"continue\"),\n })\n .merge(RetrySchema);\n\nexport type ModelRetryMiddlewareConfig = z.input<\n typeof ModelRetryMiddlewareOptionsSchema\n>;\n\n/**\n * Middleware that automatically retries failed model calls with configurable backoff.\n *\n * Supports retrying on specific exceptions and exponential backoff.\n *\n * @example Basic usage with default settings (2 retries, exponential backoff)\n * ```ts\n * import { createAgent, modelRetryMiddleware } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [searchTool],\n * middleware: [modelRetryMiddleware()],\n * });\n * ```\n *\n * @example Retry specific exceptions only\n * ```ts\n * import { modelRetryMiddleware } from \"langchain\";\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * retryOn: [TimeoutError, NetworkError],\n * backoffFactor: 1.5,\n * });\n * ```\n *\n * @example Custom exception filtering\n * ```ts\n * function shouldRetry(error: Error): boolean {\n * // Only retry on rate limit errors\n * if (error.name === \"RateLimitError\") {\n * return true;\n * }\n * // Or check for specific HTTP status codes\n * if (error.name === \"HTTPError\" && \"statusCode\" in error) {\n * const statusCode = (error as any).statusCode;\n * return statusCode === 429 || statusCode === 503;\n * }\n * return false;\n * }\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 3,\n * retryOn: shouldRetry,\n * });\n * ```\n *\n * @example Return error message instead of raising\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * onFailure: \"continue\", // Return AIMessage with error instead of throwing\n * });\n * ```\n *\n * @example Custom error message formatting\n * ```ts\n * const formatError = (error: Error) =>\n * `Model call failed: ${error.message}. Please try again later.`;\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * onFailure: formatError,\n * });\n * ```\n *\n * @example Constant backoff (no exponential growth)\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 5,\n * backoffFactor: 0.0, // No exponential growth\n * initialDelayMs: 2000, // Always wait 2 seconds\n * });\n * ```\n *\n * @example Raise exception on failure\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 2,\n * onFailure: \"error\", // Re-raise exception instead of returning message\n * });\n * ```\n *\n * @param config - Configuration options for the retry middleware\n * @returns A middleware instance that handles model failures with retries\n */\nexport function modelRetryMiddleware(config: ModelRetryMiddlewareConfig = {}) {\n const { success, error, data } =\n ModelRetryMiddlewareOptionsSchema.safeParse(config);\n if (!success) {\n throw new InvalidRetryConfigError(error);\n }\n const {\n maxRetries,\n retryOn,\n onFailure,\n backoffFactor,\n initialDelayMs,\n maxDelayMs,\n jitter,\n } = data;\n\n /**\n * Check if the exception should trigger a retry.\n */\n const shouldRetryException = (error: Error): boolean => {\n if (typeof retryOn === \"function\") {\n return retryOn(error);\n }\n // retryOn is an array of error constructors\n return retryOn.some(\n (ErrorConstructor) => error.constructor === ErrorConstructor\n );\n };\n\n // Use the exported calculateRetryDelay function with our config\n const delayConfig = { backoffFactor, initialDelayMs, maxDelayMs, jitter };\n\n /**\n * Format the failure message when retries are exhausted.\n */\n const formatFailureMessage = (error: Error, attemptsMade: number): string => {\n const errorType = error.constructor.name;\n const attemptWord = attemptsMade === 1 ? \"attempt\" : \"attempts\";\n return `Model call failed after ${attemptsMade} ${attemptWord} with ${errorType}: ${error.message}`;\n };\n\n /**\n * Handle failure when all retries are exhausted.\n */\n const handleFailure = (error: Error, attemptsMade: number): AIMessage => {\n if (onFailure === \"error\") {\n throw error;\n }\n\n let content: string;\n if (typeof onFailure === \"function\") {\n content = onFailure(error);\n } else {\n content = formatFailureMessage(error, attemptsMade);\n }\n\n return new AIMessage({\n content,\n });\n };\n\n return createMiddleware({\n name: \"modelRetryMiddleware\",\n contextSchema: ModelRetryMiddlewareOptionsSchema,\n wrapModelCall: async (request, handler) => {\n // Initial attempt + retries\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await handler(request);\n } catch (error) {\n const attemptsMade = attempt + 1; // attempt is 0-indexed\n\n // Ensure error is an Error instance\n const err =\n error && typeof error === \"object\" && \"message\" in error\n ? (error as Error)\n : new Error(String(error));\n\n // Check if we should retry this exception\n if (!shouldRetryException(err)) {\n // Exception is not retryable, handle failure immediately\n return handleFailure(err, attemptsMade);\n }\n\n // Check if we have more retries left\n if (attempt < maxRetries) {\n // Calculate and apply backoff delay\n const delay = calculateRetryDelay(delayConfig, attempt);\n if (delay > 0) {\n await sleep(delay);\n }\n // Continue to next retry\n } else {\n // No more retries, handle failure\n return handleFailure(err, attemptsMade);\n }\n }\n }\n\n // Unreachable: loop always returns via handler success or handleFailure\n throw new Error(\"Unexpected: retry loop completed without returning\");\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,oCAAoCA,OAAAA,EAC9C,OAAO,
|
|
1
|
+
{"version":3,"file":"modelRetry.cjs","names":["z","RetrySchema","InvalidRetryConfigError","AIMessage","createMiddleware","calculateRetryDelay","sleep"],"sources":["../../../src/agents/middleware/modelRetry.ts"],"sourcesContent":["/**\n * Model retry middleware for agents.\n */\nimport { z } from \"zod/v3\";\nimport { AIMessage } from \"@langchain/core/messages\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { sleep, calculateRetryDelay } from \"./utils.js\";\nimport { RetrySchema } from \"./constants.js\";\nimport { InvalidRetryConfigError } from \"./error.js\";\n\n/**\n * Configuration options for the Model Retry Middleware.\n */\nexport const ModelRetryMiddlewareOptionsSchema = z\n .object({\n /**\n * Behavior when all retries are exhausted. Options:\n * - `\"continue\"` (default): Return an AIMessage with error details, allowing\n * the agent to potentially handle the failure gracefully.\n * - `\"error\"`: Re-raise the exception, stopping agent execution.\n * - Custom function: Function that takes the exception and returns a string\n * for the AIMessage content, allowing custom error formatting.\n */\n onFailure: z\n .union([\n z.literal(\"error\"),\n z.literal(\"continue\"),\n z.function().args(z.instanceof(Error)).returns(z.string()),\n ])\n .default(\"continue\"),\n })\n .merge(RetrySchema);\n\nexport type ModelRetryMiddlewareConfig = z.input<\n typeof ModelRetryMiddlewareOptionsSchema\n>;\n\n/**\n * Middleware that automatically retries failed model calls with configurable backoff.\n *\n * Supports retrying on specific exceptions and exponential backoff.\n *\n * @example Basic usage with default settings (2 retries, exponential backoff)\n * ```ts\n * import { createAgent, modelRetryMiddleware } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [searchTool],\n * middleware: [modelRetryMiddleware()],\n * });\n * ```\n *\n * @example Retry specific exceptions only\n * ```ts\n * import { modelRetryMiddleware } from \"langchain\";\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * retryOn: [TimeoutError, NetworkError],\n * backoffFactor: 1.5,\n * });\n * ```\n *\n * @example Custom exception filtering\n * ```ts\n * function shouldRetry(error: Error): boolean {\n * // Only retry on rate limit errors\n * if (error.name === \"RateLimitError\") {\n * return true;\n * }\n * // Or check for specific HTTP status codes\n * if (error.name === \"HTTPError\" && \"statusCode\" in error) {\n * const statusCode = (error as any).statusCode;\n * return statusCode === 429 || statusCode === 503;\n * }\n * return false;\n * }\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 3,\n * retryOn: shouldRetry,\n * });\n * ```\n *\n * @example Return error message instead of raising\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * onFailure: \"continue\", // Return AIMessage with error instead of throwing\n * });\n * ```\n *\n * @example Custom error message formatting\n * ```ts\n * const formatError = (error: Error) =>\n * `Model call failed: ${error.message}. Please try again later.`;\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * onFailure: formatError,\n * });\n * ```\n *\n * @example Constant backoff (no exponential growth)\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 5,\n * backoffFactor: 0.0, // No exponential growth\n * initialDelayMs: 2000, // Always wait 2 seconds\n * });\n * ```\n *\n * @example Raise exception on failure\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 2,\n * onFailure: \"error\", // Re-raise exception instead of returning message\n * });\n * ```\n *\n * @param config - Configuration options for the retry middleware\n * @returns A middleware instance that handles model failures with retries\n */\nexport function modelRetryMiddleware(config: ModelRetryMiddlewareConfig = {}) {\n const { success, error, data } =\n ModelRetryMiddlewareOptionsSchema.safeParse(config);\n if (!success) {\n throw new InvalidRetryConfigError(error);\n }\n const {\n maxRetries,\n retryOn,\n onFailure,\n backoffFactor,\n initialDelayMs,\n maxDelayMs,\n jitter,\n } = data;\n\n /**\n * Check if the exception should trigger a retry.\n */\n const shouldRetryException = (error: Error): boolean => {\n if (typeof retryOn === \"function\") {\n return retryOn(error);\n }\n // retryOn is an array of error constructors\n return retryOn.some(\n (ErrorConstructor) => error.constructor === ErrorConstructor\n );\n };\n\n // Use the exported calculateRetryDelay function with our config\n const delayConfig = { backoffFactor, initialDelayMs, maxDelayMs, jitter };\n\n /**\n * Format the failure message when retries are exhausted.\n */\n const formatFailureMessage = (error: Error, attemptsMade: number): string => {\n const errorType = error.constructor.name;\n const attemptWord = attemptsMade === 1 ? \"attempt\" : \"attempts\";\n return `Model call failed after ${attemptsMade} ${attemptWord} with ${errorType}: ${error.message}`;\n };\n\n /**\n * Handle failure when all retries are exhausted.\n */\n const handleFailure = (error: Error, attemptsMade: number): AIMessage => {\n if (onFailure === \"error\") {\n throw error;\n }\n\n let content: string;\n if (typeof onFailure === \"function\") {\n content = onFailure(error);\n } else {\n content = formatFailureMessage(error, attemptsMade);\n }\n\n return new AIMessage({\n content,\n });\n };\n\n return createMiddleware({\n name: \"modelRetryMiddleware\",\n contextSchema: ModelRetryMiddlewareOptionsSchema,\n wrapModelCall: async (request, handler) => {\n // Initial attempt + retries\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await handler(request);\n } catch (error) {\n const attemptsMade = attempt + 1; // attempt is 0-indexed\n\n // Ensure error is an Error instance\n const err =\n error && typeof error === \"object\" && \"message\" in error\n ? (error as Error)\n : new Error(String(error));\n\n // Check if we should retry this exception\n if (!shouldRetryException(err)) {\n // Exception is not retryable, handle failure immediately\n return handleFailure(err, attemptsMade);\n }\n\n // Check if we have more retries left\n if (attempt < maxRetries) {\n // Calculate and apply backoff delay\n const delay = calculateRetryDelay(delayConfig, attempt);\n if (delay > 0) {\n await sleep(delay);\n }\n // Continue to next retry\n } else {\n // No more retries, handle failure\n return handleFailure(err, attemptsMade);\n }\n }\n }\n\n // Unreachable: loop always returns via handler success or handleFailure\n throw new Error(\"Unexpected: retry loop completed without returning\");\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,oCAAoCA,OAAAA,EAC9C,OAAO;;;;;;;;;AASN,WAAWA,OAAAA,EACR,MAAM;CACLA,OAAAA,EAAE,QAAQ,QAAQ;CAClBA,OAAAA,EAAE,QAAQ,WAAW;CACrBA,OAAAA,EAAE,UAAU,CAAC,KAAKA,OAAAA,EAAE,WAAW,MAAM,CAAC,CAAC,QAAQA,OAAAA,EAAE,QAAQ,CAAC;CAC3D,CAAC,CACD,QAAQ,WAAW,EACvB,CAAC,CACD,MAAMC,kBAAAA,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FrB,SAAgB,qBAAqB,SAAqC,EAAE,EAAE;CAC5E,MAAM,EAAE,SAAS,OAAO,SACtB,kCAAkC,UAAU,OAAO;AACrD,KAAI,CAAC,QACH,OAAM,IAAIC,cAAAA,wBAAwB,MAAM;CAE1C,MAAM,EACJ,YACA,SACA,WACA,eACA,gBACA,YACA,WACE;;;;CAKJ,MAAM,wBAAwB,UAA0B;AACtD,MAAI,OAAO,YAAY,WACrB,QAAO,QAAQ,MAAM;AAGvB,SAAO,QAAQ,MACZ,qBAAqB,MAAM,gBAAgB,iBAC7C;;CAIH,MAAM,cAAc;EAAE;EAAe;EAAgB;EAAY;EAAQ;;;;CAKzE,MAAM,wBAAwB,OAAc,iBAAiC;EAC3E,MAAM,YAAY,MAAM,YAAY;AAEpC,SAAO,2BAA2B,aAAa,GAD3B,iBAAiB,IAAI,YAAY,WACS,QAAQ,UAAU,IAAI,MAAM;;;;;CAM5F,MAAM,iBAAiB,OAAc,iBAAoC;AACvE,MAAI,cAAc,QAChB,OAAM;EAGR,IAAI;AACJ,MAAI,OAAO,cAAc,WACvB,WAAU,UAAU,MAAM;MAE1B,WAAU,qBAAqB,OAAO,aAAa;AAGrD,SAAO,IAAIC,yBAAAA,UAAU,EACnB,SACD,CAAC;;AAGJ,QAAOC,mBAAAA,iBAAiB;EACtB,MAAM;EACN,eAAe;EACf,eAAe,OAAO,SAAS,YAAY;AAEzC,QAAK,IAAI,UAAU,GAAG,WAAW,YAAY,UAC3C,KAAI;AACF,WAAO,MAAM,QAAQ,QAAQ;YACtB,OAAO;IACd,MAAM,eAAe,UAAU;IAG/B,MAAM,MACJ,SAAS,OAAO,UAAU,YAAY,aAAa,QAC9C,QACD,IAAI,MAAM,OAAO,MAAM,CAAC;AAG9B,QAAI,CAAC,qBAAqB,IAAI,CAE5B,QAAO,cAAc,KAAK,aAAa;AAIzC,QAAI,UAAU,YAAY;KAExB,MAAM,QAAQC,cAAAA,oBAAoB,aAAa,QAAQ;AACvD,SAAI,QAAQ,EACV,OAAMC,cAAAA,MAAM,MAAM;UAKpB,QAAO,cAAc,KAAK,aAAa;;AAM7C,SAAM,IAAI,MAAM,qDAAqD;;EAExE,CAAC"}
|
|
@@ -11,7 +11,16 @@ import { z } from "zod/v3";
|
|
|
11
11
|
/**
|
|
12
12
|
* Configuration options for the Model Retry Middleware.
|
|
13
13
|
*/
|
|
14
|
-
const ModelRetryMiddlewareOptionsSchema = z.object({
|
|
14
|
+
const ModelRetryMiddlewareOptionsSchema = z.object({
|
|
15
|
+
/**
|
|
16
|
+
* Behavior when all retries are exhausted. Options:
|
|
17
|
+
* - `"continue"` (default): Return an AIMessage with error details, allowing
|
|
18
|
+
* the agent to potentially handle the failure gracefully.
|
|
19
|
+
* - `"error"`: Re-raise the exception, stopping agent execution.
|
|
20
|
+
* - Custom function: Function that takes the exception and returns a string
|
|
21
|
+
* for the AIMessage content, allowing custom error formatting.
|
|
22
|
+
*/
|
|
23
|
+
onFailure: z.union([
|
|
15
24
|
z.literal("error"),
|
|
16
25
|
z.literal("continue"),
|
|
17
26
|
z.function().args(z.instanceof(Error)).returns(z.string())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modelRetry.js","names":[],"sources":["../../../src/agents/middleware/modelRetry.ts"],"sourcesContent":["/**\n * Model retry middleware for agents.\n */\nimport { z } from \"zod/v3\";\nimport { AIMessage } from \"@langchain/core/messages\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { sleep, calculateRetryDelay } from \"./utils.js\";\nimport { RetrySchema } from \"./constants.js\";\nimport { InvalidRetryConfigError } from \"./error.js\";\n\n/**\n * Configuration options for the Model Retry Middleware.\n */\nexport const ModelRetryMiddlewareOptionsSchema = z\n .object({\n /**\n * Behavior when all retries are exhausted. Options:\n * - `\"continue\"` (default): Return an AIMessage with error details, allowing\n * the agent to potentially handle the failure gracefully.\n * - `\"error\"`: Re-raise the exception, stopping agent execution.\n * - Custom function: Function that takes the exception and returns a string\n * for the AIMessage content, allowing custom error formatting.\n */\n onFailure: z\n .union([\n z.literal(\"error\"),\n z.literal(\"continue\"),\n z.function().args(z.instanceof(Error)).returns(z.string()),\n ])\n .default(\"continue\"),\n })\n .merge(RetrySchema);\n\nexport type ModelRetryMiddlewareConfig = z.input<\n typeof ModelRetryMiddlewareOptionsSchema\n>;\n\n/**\n * Middleware that automatically retries failed model calls with configurable backoff.\n *\n * Supports retrying on specific exceptions and exponential backoff.\n *\n * @example Basic usage with default settings (2 retries, exponential backoff)\n * ```ts\n * import { createAgent, modelRetryMiddleware } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [searchTool],\n * middleware: [modelRetryMiddleware()],\n * });\n * ```\n *\n * @example Retry specific exceptions only\n * ```ts\n * import { modelRetryMiddleware } from \"langchain\";\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * retryOn: [TimeoutError, NetworkError],\n * backoffFactor: 1.5,\n * });\n * ```\n *\n * @example Custom exception filtering\n * ```ts\n * function shouldRetry(error: Error): boolean {\n * // Only retry on rate limit errors\n * if (error.name === \"RateLimitError\") {\n * return true;\n * }\n * // Or check for specific HTTP status codes\n * if (error.name === \"HTTPError\" && \"statusCode\" in error) {\n * const statusCode = (error as any).statusCode;\n * return statusCode === 429 || statusCode === 503;\n * }\n * return false;\n * }\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 3,\n * retryOn: shouldRetry,\n * });\n * ```\n *\n * @example Return error message instead of raising\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * onFailure: \"continue\", // Return AIMessage with error instead of throwing\n * });\n * ```\n *\n * @example Custom error message formatting\n * ```ts\n * const formatError = (error: Error) =>\n * `Model call failed: ${error.message}. Please try again later.`;\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * onFailure: formatError,\n * });\n * ```\n *\n * @example Constant backoff (no exponential growth)\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 5,\n * backoffFactor: 0.0, // No exponential growth\n * initialDelayMs: 2000, // Always wait 2 seconds\n * });\n * ```\n *\n * @example Raise exception on failure\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 2,\n * onFailure: \"error\", // Re-raise exception instead of returning message\n * });\n * ```\n *\n * @param config - Configuration options for the retry middleware\n * @returns A middleware instance that handles model failures with retries\n */\nexport function modelRetryMiddleware(config: ModelRetryMiddlewareConfig = {}) {\n const { success, error, data } =\n ModelRetryMiddlewareOptionsSchema.safeParse(config);\n if (!success) {\n throw new InvalidRetryConfigError(error);\n }\n const {\n maxRetries,\n retryOn,\n onFailure,\n backoffFactor,\n initialDelayMs,\n maxDelayMs,\n jitter,\n } = data;\n\n /**\n * Check if the exception should trigger a retry.\n */\n const shouldRetryException = (error: Error): boolean => {\n if (typeof retryOn === \"function\") {\n return retryOn(error);\n }\n // retryOn is an array of error constructors\n return retryOn.some(\n (ErrorConstructor) => error.constructor === ErrorConstructor\n );\n };\n\n // Use the exported calculateRetryDelay function with our config\n const delayConfig = { backoffFactor, initialDelayMs, maxDelayMs, jitter };\n\n /**\n * Format the failure message when retries are exhausted.\n */\n const formatFailureMessage = (error: Error, attemptsMade: number): string => {\n const errorType = error.constructor.name;\n const attemptWord = attemptsMade === 1 ? \"attempt\" : \"attempts\";\n return `Model call failed after ${attemptsMade} ${attemptWord} with ${errorType}: ${error.message}`;\n };\n\n /**\n * Handle failure when all retries are exhausted.\n */\n const handleFailure = (error: Error, attemptsMade: number): AIMessage => {\n if (onFailure === \"error\") {\n throw error;\n }\n\n let content: string;\n if (typeof onFailure === \"function\") {\n content = onFailure(error);\n } else {\n content = formatFailureMessage(error, attemptsMade);\n }\n\n return new AIMessage({\n content,\n });\n };\n\n return createMiddleware({\n name: \"modelRetryMiddleware\",\n contextSchema: ModelRetryMiddlewareOptionsSchema,\n wrapModelCall: async (request, handler) => {\n // Initial attempt + retries\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await handler(request);\n } catch (error) {\n const attemptsMade = attempt + 1; // attempt is 0-indexed\n\n // Ensure error is an Error instance\n const err =\n error && typeof error === \"object\" && \"message\" in error\n ? (error as Error)\n : new Error(String(error));\n\n // Check if we should retry this exception\n if (!shouldRetryException(err)) {\n // Exception is not retryable, handle failure immediately\n return handleFailure(err, attemptsMade);\n }\n\n // Check if we have more retries left\n if (attempt < maxRetries) {\n // Calculate and apply backoff delay\n const delay = calculateRetryDelay(delayConfig, attempt);\n if (delay > 0) {\n await sleep(delay);\n }\n // Continue to next retry\n } else {\n // No more retries, handle failure\n return handleFailure(err, attemptsMade);\n }\n }\n }\n\n // Unreachable: loop always returns via handler success or handleFailure\n throw new Error(\"Unexpected: retry loop completed without returning\");\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,MAAa,oCAAoC,EAC9C,OAAO,
|
|
1
|
+
{"version":3,"file":"modelRetry.js","names":[],"sources":["../../../src/agents/middleware/modelRetry.ts"],"sourcesContent":["/**\n * Model retry middleware for agents.\n */\nimport { z } from \"zod/v3\";\nimport { AIMessage } from \"@langchain/core/messages\";\n\nimport { createMiddleware } from \"../middleware.js\";\nimport { sleep, calculateRetryDelay } from \"./utils.js\";\nimport { RetrySchema } from \"./constants.js\";\nimport { InvalidRetryConfigError } from \"./error.js\";\n\n/**\n * Configuration options for the Model Retry Middleware.\n */\nexport const ModelRetryMiddlewareOptionsSchema = z\n .object({\n /**\n * Behavior when all retries are exhausted. Options:\n * - `\"continue\"` (default): Return an AIMessage with error details, allowing\n * the agent to potentially handle the failure gracefully.\n * - `\"error\"`: Re-raise the exception, stopping agent execution.\n * - Custom function: Function that takes the exception and returns a string\n * for the AIMessage content, allowing custom error formatting.\n */\n onFailure: z\n .union([\n z.literal(\"error\"),\n z.literal(\"continue\"),\n z.function().args(z.instanceof(Error)).returns(z.string()),\n ])\n .default(\"continue\"),\n })\n .merge(RetrySchema);\n\nexport type ModelRetryMiddlewareConfig = z.input<\n typeof ModelRetryMiddlewareOptionsSchema\n>;\n\n/**\n * Middleware that automatically retries failed model calls with configurable backoff.\n *\n * Supports retrying on specific exceptions and exponential backoff.\n *\n * @example Basic usage with default settings (2 retries, exponential backoff)\n * ```ts\n * import { createAgent, modelRetryMiddleware } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [searchTool],\n * middleware: [modelRetryMiddleware()],\n * });\n * ```\n *\n * @example Retry specific exceptions only\n * ```ts\n * import { modelRetryMiddleware } from \"langchain\";\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * retryOn: [TimeoutError, NetworkError],\n * backoffFactor: 1.5,\n * });\n * ```\n *\n * @example Custom exception filtering\n * ```ts\n * function shouldRetry(error: Error): boolean {\n * // Only retry on rate limit errors\n * if (error.name === \"RateLimitError\") {\n * return true;\n * }\n * // Or check for specific HTTP status codes\n * if (error.name === \"HTTPError\" && \"statusCode\" in error) {\n * const statusCode = (error as any).statusCode;\n * return statusCode === 429 || statusCode === 503;\n * }\n * return false;\n * }\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 3,\n * retryOn: shouldRetry,\n * });\n * ```\n *\n * @example Return error message instead of raising\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * onFailure: \"continue\", // Return AIMessage with error instead of throwing\n * });\n * ```\n *\n * @example Custom error message formatting\n * ```ts\n * const formatError = (error: Error) =>\n * `Model call failed: ${error.message}. Please try again later.`;\n *\n * const retry = modelRetryMiddleware({\n * maxRetries: 4,\n * onFailure: formatError,\n * });\n * ```\n *\n * @example Constant backoff (no exponential growth)\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 5,\n * backoffFactor: 0.0, // No exponential growth\n * initialDelayMs: 2000, // Always wait 2 seconds\n * });\n * ```\n *\n * @example Raise exception on failure\n * ```ts\n * const retry = modelRetryMiddleware({\n * maxRetries: 2,\n * onFailure: \"error\", // Re-raise exception instead of returning message\n * });\n * ```\n *\n * @param config - Configuration options for the retry middleware\n * @returns A middleware instance that handles model failures with retries\n */\nexport function modelRetryMiddleware(config: ModelRetryMiddlewareConfig = {}) {\n const { success, error, data } =\n ModelRetryMiddlewareOptionsSchema.safeParse(config);\n if (!success) {\n throw new InvalidRetryConfigError(error);\n }\n const {\n maxRetries,\n retryOn,\n onFailure,\n backoffFactor,\n initialDelayMs,\n maxDelayMs,\n jitter,\n } = data;\n\n /**\n * Check if the exception should trigger a retry.\n */\n const shouldRetryException = (error: Error): boolean => {\n if (typeof retryOn === \"function\") {\n return retryOn(error);\n }\n // retryOn is an array of error constructors\n return retryOn.some(\n (ErrorConstructor) => error.constructor === ErrorConstructor\n );\n };\n\n // Use the exported calculateRetryDelay function with our config\n const delayConfig = { backoffFactor, initialDelayMs, maxDelayMs, jitter };\n\n /**\n * Format the failure message when retries are exhausted.\n */\n const formatFailureMessage = (error: Error, attemptsMade: number): string => {\n const errorType = error.constructor.name;\n const attemptWord = attemptsMade === 1 ? \"attempt\" : \"attempts\";\n return `Model call failed after ${attemptsMade} ${attemptWord} with ${errorType}: ${error.message}`;\n };\n\n /**\n * Handle failure when all retries are exhausted.\n */\n const handleFailure = (error: Error, attemptsMade: number): AIMessage => {\n if (onFailure === \"error\") {\n throw error;\n }\n\n let content: string;\n if (typeof onFailure === \"function\") {\n content = onFailure(error);\n } else {\n content = formatFailureMessage(error, attemptsMade);\n }\n\n return new AIMessage({\n content,\n });\n };\n\n return createMiddleware({\n name: \"modelRetryMiddleware\",\n contextSchema: ModelRetryMiddlewareOptionsSchema,\n wrapModelCall: async (request, handler) => {\n // Initial attempt + retries\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await handler(request);\n } catch (error) {\n const attemptsMade = attempt + 1; // attempt is 0-indexed\n\n // Ensure error is an Error instance\n const err =\n error && typeof error === \"object\" && \"message\" in error\n ? (error as Error)\n : new Error(String(error));\n\n // Check if we should retry this exception\n if (!shouldRetryException(err)) {\n // Exception is not retryable, handle failure immediately\n return handleFailure(err, attemptsMade);\n }\n\n // Check if we have more retries left\n if (attempt < maxRetries) {\n // Calculate and apply backoff delay\n const delay = calculateRetryDelay(delayConfig, attempt);\n if (delay > 0) {\n await sleep(delay);\n }\n // Continue to next retry\n } else {\n // No more retries, handle failure\n return handleFailure(err, attemptsMade);\n }\n }\n }\n\n // Unreachable: loop always returns via handler success or handleFailure\n throw new Error(\"Unexpected: retry loop completed without returning\");\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,MAAa,oCAAoC,EAC9C,OAAO;;;;;;;;;AASN,WAAW,EACR,MAAM;CACL,EAAE,QAAQ,QAAQ;CAClB,EAAE,QAAQ,WAAW;CACrB,EAAE,UAAU,CAAC,KAAK,EAAE,WAAW,MAAM,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC3D,CAAC,CACD,QAAQ,WAAW,EACvB,CAAC,CACD,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FrB,SAAgB,qBAAqB,SAAqC,EAAE,EAAE;CAC5E,MAAM,EAAE,SAAS,OAAO,SACtB,kCAAkC,UAAU,OAAO;AACrD,KAAI,CAAC,QACH,OAAM,IAAI,wBAAwB,MAAM;CAE1C,MAAM,EACJ,YACA,SACA,WACA,eACA,gBACA,YACA,WACE;;;;CAKJ,MAAM,wBAAwB,UAA0B;AACtD,MAAI,OAAO,YAAY,WACrB,QAAO,QAAQ,MAAM;AAGvB,SAAO,QAAQ,MACZ,qBAAqB,MAAM,gBAAgB,iBAC7C;;CAIH,MAAM,cAAc;EAAE;EAAe;EAAgB;EAAY;EAAQ;;;;CAKzE,MAAM,wBAAwB,OAAc,iBAAiC;EAC3E,MAAM,YAAY,MAAM,YAAY;AAEpC,SAAO,2BAA2B,aAAa,GAD3B,iBAAiB,IAAI,YAAY,WACS,QAAQ,UAAU,IAAI,MAAM;;;;;CAM5F,MAAM,iBAAiB,OAAc,iBAAoC;AACvE,MAAI,cAAc,QAChB,OAAM;EAGR,IAAI;AACJ,MAAI,OAAO,cAAc,WACvB,WAAU,UAAU,MAAM;MAE1B,WAAU,qBAAqB,OAAO,aAAa;AAGrD,SAAO,IAAI,UAAU,EACnB,SACD,CAAC;;AAGJ,QAAO,iBAAiB;EACtB,MAAM;EACN,eAAe;EACf,eAAe,OAAO,SAAS,YAAY;AAEzC,QAAK,IAAI,UAAU,GAAG,WAAW,YAAY,UAC3C,KAAI;AACF,WAAO,MAAM,QAAQ,QAAQ;YACtB,OAAO;IACd,MAAM,eAAe,UAAU;IAG/B,MAAM,MACJ,SAAS,OAAO,UAAU,YAAY,aAAa,QAC9C,QACD,IAAI,MAAM,OAAO,MAAM,CAAC;AAG9B,QAAI,CAAC,qBAAqB,IAAI,CAE5B,QAAO,cAAc,KAAK,aAAa;AAIzC,QAAI,UAAU,YAAY;KAExB,MAAM,QAAQ,oBAAoB,aAAa,QAAQ;AACvD,SAAI,QAAQ,EACV,OAAM,MAAM,MAAM;UAKpB,QAAO,cAAc,KAAK,aAAa;;AAM7C,SAAM,IAAI,MAAM,qDAAqD;;EAExE,CAAC"}
|
|
@@ -228,8 +228,17 @@ function applyStrategy(content, matches, strategy, piiType) {
|
|
|
228
228
|
* Configuration schema for PII middleware
|
|
229
229
|
*/
|
|
230
230
|
const contextSchema = zod_v3.z.object({
|
|
231
|
+
/**
|
|
232
|
+
* Whether to check user messages before model call
|
|
233
|
+
*/
|
|
231
234
|
applyToInput: zod_v3.z.boolean().optional(),
|
|
235
|
+
/**
|
|
236
|
+
* Whether to check AI messages after model call
|
|
237
|
+
*/
|
|
232
238
|
applyToOutput: zod_v3.z.boolean().optional(),
|
|
239
|
+
/**
|
|
240
|
+
* Whether to check tool result messages after tool execution
|
|
241
|
+
*/
|
|
233
242
|
applyToToolResults: zod_v3.z.boolean().optional()
|
|
234
243
|
});
|
|
235
244
|
/**
|
|
@@ -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":["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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"}
|