langchain 1.5.10 → 1.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -1
- package/dist/agents/ReactAgent.cjs.map +1 -1
- package/dist/agents/ReactAgent.js.map +1 -1
- package/dist/agents/RunnableCallable.cjs.map +1 -1
- package/dist/agents/RunnableCallable.js.map +1 -1
- package/dist/agents/annotation.cjs.map +1 -1
- package/dist/agents/errors.cjs.map +1 -1
- package/dist/agents/middleware/constants.cjs.map +1 -1
- package/dist/agents/middleware/contextEditing.cjs.map +1 -1
- package/dist/agents/middleware/contextEditing.js.map +1 -1
- package/dist/agents/middleware/hitl.cjs +2 -1
- package/dist/agents/middleware/hitl.cjs.map +1 -1
- package/dist/agents/middleware/hitl.js +2 -1
- package/dist/agents/middleware/hitl.js.map +1 -1
- package/dist/agents/middleware/llmToolSelector.cjs +2 -1
- package/dist/agents/middleware/llmToolSelector.cjs.map +1 -1
- package/dist/agents/middleware/llmToolSelector.js +2 -1
- package/dist/agents/middleware/llmToolSelector.js.map +1 -1
- package/dist/agents/middleware/pii.cjs +17 -15
- package/dist/agents/middleware/pii.cjs.map +1 -1
- package/dist/agents/middleware/pii.js +17 -15
- package/dist/agents/middleware/pii.js.map +1 -1
- package/dist/agents/middleware/providerToolSearch.cjs.map +1 -1
- package/dist/agents/middleware/summarization.cjs +9 -2
- package/dist/agents/middleware/summarization.cjs.map +1 -1
- package/dist/agents/middleware/summarization.js +9 -2
- package/dist/agents/middleware/summarization.js.map +1 -1
- package/dist/agents/middleware/todoListMiddleware.cjs.map +1 -1
- package/dist/agents/middleware/toolCallLimit.cjs +5 -1
- package/dist/agents/middleware/toolCallLimit.cjs.map +1 -1
- package/dist/agents/middleware/toolCallLimit.js +5 -1
- package/dist/agents/middleware/toolCallLimit.js.map +1 -1
- package/dist/agents/middleware/toolEmulator.cjs +4 -2
- package/dist/agents/middleware/toolEmulator.cjs.map +1 -1
- package/dist/agents/middleware/toolEmulator.js +4 -2
- package/dist/agents/middleware/toolEmulator.js.map +1 -1
- package/dist/agents/middleware/toolError.cjs.map +1 -1
- package/dist/agents/middleware/utils.cjs.map +1 -1
- package/dist/agents/nodes/AgentNode.cjs +4 -1
- package/dist/agents/nodes/AgentNode.cjs.map +1 -1
- package/dist/agents/nodes/AgentNode.js +4 -1
- package/dist/agents/nodes/AgentNode.js.map +1 -1
- package/dist/agents/nodes/ToolNode.cjs +9 -7
- package/dist/agents/nodes/ToolNode.cjs.map +1 -1
- package/dist/agents/nodes/ToolNode.js +9 -7
- package/dist/agents/nodes/ToolNode.js.map +1 -1
- package/dist/agents/nodes/middleware.cjs.map +1 -1
- package/dist/agents/nodes/utils.cjs +2 -1
- package/dist/agents/nodes/utils.cjs.map +1 -1
- package/dist/agents/nodes/utils.js +2 -1
- package/dist/agents/nodes/utils.js.map +1 -1
- package/dist/agents/responses.cjs +1 -1
- package/dist/agents/responses.cjs.map +1 -1
- package/dist/agents/responses.d.cts.map +1 -1
- package/dist/agents/responses.d.ts.map +1 -1
- package/dist/agents/responses.js +1 -1
- package/dist/agents/responses.js.map +1 -1
- package/dist/agents/transformers/subagent.cjs.map +1 -1
- package/dist/agents/transformers/subagent.js.map +1 -1
- package/dist/agents/transformers/tool-call.cjs.map +1 -1
- package/dist/agents/transformers/tool-call.js.map +1 -1
- package/dist/agents/types.d.cts.map +1 -1
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/agents/utils.cjs.map +1 -1
- package/dist/chat_models/universal.cjs +12 -10
- package/dist/chat_models/universal.cjs.map +1 -1
- package/dist/chat_models/universal.js +12 -10
- package/dist/chat_models/universal.js.map +1 -1
- package/dist/hub/base.cjs.map +1 -1
- package/dist/hub/base.js.map +1 -1
- package/dist/hub/index.cjs +2 -1
- package/dist/hub/index.cjs.map +1 -1
- package/dist/hub/index.js +2 -1
- package/dist/hub/index.js.map +1 -1
- package/dist/hub/node.cjs +4 -2
- package/dist/hub/node.cjs.map +1 -1
- package/dist/hub/node.js +4 -2
- package/dist/hub/node.js.map +1 -1
- package/dist/load/index.cjs.map +1 -1
- package/dist/tools/headless.cjs.map +1 -1
- package/package.json +18 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pii.js","names":[],"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.js","names":[],"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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providerToolSearch.cjs","names":["createMiddleware","isLangChainTool"],"sources":["../../../src/agents/middleware/providerToolSearch.ts"],"sourcesContent":["import type {\n StructuredToolInterface,\n StructuredToolParams,\n ClientTool,\n ServerTool,\n} from \"@langchain/core/tools\";\nimport { isLangChainTool } from \"@langchain/core/utils/function_calling\";\nimport { createMiddleware } from \"../middleware.js\";\nimport type { LanguageModelLike } from \"@langchain/core/language_models/base\";\nimport type {\n ChatModelProvider,\n ConfigurableModel,\n} from \"../../chat_models/universal.js\";\n\ntype ToolName = string;\nexport type ToolIdentifier = ToolName | StructuredToolInterface;\n\nexport type ProviderToolSearchMiddlewareConfig = {\n /**\n * Which tools are deferred; withheld from the model until its tool search surfaces them.\n *\n * Tools already constructed with `extras.defer_loading === true` are deferred\n * regardless of this option; if `searchableTools` is omitted, only those pre-marked\n * tools are deferred.\n */\n searchableTools?: ToolIdentifier[];\n};\n\nconst SERVER_SEARCH_PROVIDERS = [\n \"anthropic\",\n \"openai\",\n] as const satisfies readonly ChatModelProvider[];\n\ntype ServerSearchCapableProvider = (typeof SERVER_SEARCH_PROVIDERS)[number];\ntype DetectedProvider = ServerSearchCapableProvider | \"other\";\n\ntype ServerToolSearchTool = { type: string; name?: string };\n\nconst SERVER_TOOL_SEARCH_TOOLS = {\n anthropic: {\n type: \"tool_search_tool_bm25_20251119\",\n name: \"tool_search_tool_bm25\",\n },\n openai: { type: \"tool_search\" },\n} as const satisfies Record<ServerSearchCapableProvider, ServerToolSearchTool>;\n\n/**\n * Provider-side tool search middleware.\n *\n * Leverages server-side tool search: the full client tool catalog is forwarded\n * to the provider, with deferred tools marked `defer_loading` so the provider\n * discloses them on demand via its own search. A tool is deferred when it is\n * named in `searchableTools` or built with `extras.defer_loading: true`.\n *\n * Requires a model with server-side tool search support: OpenAI gpt-5.4+ or Anthropic\n * Claude Sonnet 4+/Opus 4+/Haiku 4.5+. Non-Anthropic/OpenAI providers throw; an\n * in-family model that is too old surfaces the provider's own API error rather\n * than being gated here.\n *\n * @example\n * ```ts\n * import { createAgent, providerToolSearchMiddleware } from \"langchain\";\n * import { ChatAnthropic } from \"@langchain/anthropic\";\n *\n * const agent = createAgent({\n * model: new ChatAnthropic({ model: \"claude-sonnet-4-5\" }),\n * tools: [getWeather, ...nicheTools],\n * middleware: [\n * // Defer the niche tools behind the provider's tool search; the model\n * // discovers them on demand instead of receiving every schema up front.\n * providerToolSearchMiddleware({ searchableTools: nicheTools }),\n * ],\n * });\n * ```\n *\n * @example\n * ```ts\n * import { tool } from \"@langchain/core/tools\";\n * import { createAgent, providerToolSearchMiddleware } from \"langchain\";\n * import { ChatAnthropic } from \"@langchain/anthropic\";\n *\n * // A tool marked `defer_loading` at construction is deferred on its own —\n * // no need to list it in `searchableTools`; the middleware honors the flag.\n * const sendEmail = tool(sendEmailFn, {\n * name: \"send_email\",\n * description: \"Send an email\",\n * schema: sendEmailSchema,\n * extras: { defer_loading: true },\n * });\n *\n * const agent = createAgent({\n * model: new ChatAnthropic({ model: \"claude-sonnet-4-5\" }),\n * tools: [getWeather, sendEmail],\n * middleware: [providerToolSearchMiddleware()],\n * });\n * ```\n *\n * @param config - Configuration options for the middleware\n * @param config.searchableTools - Tools to defer behind tool search\n * @returns A middleware instance that can be used with `createAgent`\n */\nexport function providerToolSearchMiddleware(\n config: ProviderToolSearchMiddlewareConfig = {}\n) {\n const deferNames = toToolNames(config.searchableTools);\n\n return createMiddleware({\n name: \"ProviderToolSearch\",\n wrapModelCall: (request, handler) => {\n const tools = request.tools ?? [];\n\n // Fail fast if we try to defer a tool that is not bound to the model\n if (deferNames.size > 0) {\n const available = tools.filter(isLangChainTool).map((t) => t.name);\n const unknown = [...deferNames].filter(\n (name) => !available.includes(name)\n );\n if (unknown.length > 0) {\n throw new Error(\n `providerToolSearchMiddleware: searchableTools references tool(s) not bound to the model: ${unknown.join(\", \")}`\n );\n }\n }\n\n const provider = getModelProvider(request.model);\n if (!supportsProviderToolSearch(provider)) {\n throw new Error(\n `providerToolSearchMiddleware requires a provider with server-side tool search, but got ${provider}`\n );\n }\n\n // Nothing to defer -> pass thru\n if (!hasDeferredTools(tools, deferNames)) return handler(request);\n\n // For each deferred tool, emit a minimal binding spec carrying `defer_loading`.\n const boundTools = tools.map((tool) =>\n deferToolIfNeeded(tool, deferNames)\n );\n\n const nativeSearchTool = SERVER_TOOL_SEARCH_TOOLS[provider];\n return handler({ ...request, tools: [...boundTools, nativeSearchTool] });\n },\n });\n}\n\nfunction isDeferred(\n tool: unknown,\n deferNames: Set<string>\n): tool is StructuredToolParams {\n return (\n isLangChainTool(tool) &&\n (tool.extras?.defer_loading === true || deferNames.has(tool.name))\n );\n}\n\nfunction hasDeferredTools(\n tools: readonly (ClientTool | ServerTool)[],\n deferNames: Set<string>\n): boolean {\n return tools.some((tool) => isDeferred(tool, deferNames));\n}\n\n/**\n * If a tool should be deferred, return a minimal binding spec carrying `defer_loading`.\n * Otherwise return it as-is\n */\nfunction deferToolIfNeeded(\n tool: ClientTool | ServerTool,\n deferNames: Set<string>\n): ClientTool | ServerTool {\n if (!isDeferred(tool, deferNames)) return tool;\n return {\n name: tool.name,\n description: tool.description,\n schema: tool.schema,\n extras: { ...tool.extras, defer_loading: true },\n };\n}\n\n/** Flatten a list of tool names/instances into a set of tool names. */\nfunction toToolNames(tools: ToolIdentifier[] = []): Set<string> {\n return new Set(tools.map((t) => (typeof t === \"string\" ? t : t.name)));\n}\n\nfunction getModelProvider(model: LanguageModelLike): DetectedProvider {\n const name = model.getName();\n const configured =\n name === \"ConfigurableModel\"\n ? (model as ConfigurableModel)._defaultConfig?.modelProvider\n : undefined;\n if (name === \"ChatAnthropic\" || configured === \"anthropic\")\n return \"anthropic\";\n if (name === \"ChatOpenAI\" || configured === \"openai\") return \"openai\";\n return \"other\";\n}\n\nfunction supportsProviderToolSearch(\n provider: DetectedProvider\n): provider is ServerSearchCapableProvider {\n return (SERVER_SEARCH_PROVIDERS as readonly string[]).includes(provider);\n}\n"],"mappings":";;;AA4BA,MAAM,0BAA0B,CAC9B,aACA,QACF;AAOA,MAAM,2BAA2B;CAC/B,WAAW;EACT,MAAM;EACN,MAAM;CACR;CACA,QAAQ,EAAE,MAAM,cAAc;AAChC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,6BACd,SAA6C,CAAC,GAC9C;CACA,MAAM,aAAa,YAAY,OAAO,eAAe;CAErD,OAAOA,mBAAAA,iBAAiB;EACtB,MAAM;EACN,gBAAgB,SAAS,YAAY;GACnC,MAAM,QAAQ,QAAQ,SAAS,CAAC;GAGhC,IAAI,WAAW,OAAO,GAAG;IACvB,MAAM,YAAY,MAAM,OAAOC,uCAAAA,eAAe,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI;IACjE,MAAM,UAAU,CAAC,GAAG,UAAU,CAAC,CAAC,QAC7B,SAAS,CAAC,UAAU,SAAS,IAAI,CACpC;IACA,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,MACR,4FAA4F,QAAQ,KAAK,IAAI,GAC/G;GAEJ;GAEA,MAAM,WAAW,iBAAiB,QAAQ,KAAK;GAC/C,IAAI,CAAC,2BAA2B,QAAQ,GACtC,MAAM,IAAI,MACR,0FAA0F,UAC5F;GAIF,IAAI,CAAC,iBAAiB,OAAO,UAAU,GAAG,OAAO,QAAQ,OAAO;GAGhE,MAAM,aAAa,MAAM,KAAK,SAC5B,kBAAkB,MAAM,UAAU,CACpC;GAEA,MAAM,mBAAmB,yBAAyB;GAClD,OAAO,QAAQ;IAAE,GAAG;IAAS,OAAO,CAAC,GAAG,YAAY,gBAAgB;GAAE,CAAC;EACzE;CACF,CAAC;AACH;AAEA,SAAS,WACP,MACA,YAC8B;CAC9B,QAAA,
|
|
1
|
+
{"version":3,"file":"providerToolSearch.cjs","names":["createMiddleware","isLangChainTool"],"sources":["../../../src/agents/middleware/providerToolSearch.ts"],"sourcesContent":["import type {\n StructuredToolInterface,\n StructuredToolParams,\n ClientTool,\n ServerTool,\n} from \"@langchain/core/tools\";\nimport { isLangChainTool } from \"@langchain/core/utils/function_calling\";\nimport { createMiddleware } from \"../middleware.js\";\nimport type { LanguageModelLike } from \"@langchain/core/language_models/base\";\nimport type {\n ChatModelProvider,\n ConfigurableModel,\n} from \"../../chat_models/universal.js\";\n\ntype ToolName = string;\nexport type ToolIdentifier = ToolName | StructuredToolInterface;\n\nexport type ProviderToolSearchMiddlewareConfig = {\n /**\n * Which tools are deferred; withheld from the model until its tool search surfaces them.\n *\n * Tools already constructed with `extras.defer_loading === true` are deferred\n * regardless of this option; if `searchableTools` is omitted, only those pre-marked\n * tools are deferred.\n */\n searchableTools?: ToolIdentifier[];\n};\n\nconst SERVER_SEARCH_PROVIDERS = [\n \"anthropic\",\n \"openai\",\n] as const satisfies readonly ChatModelProvider[];\n\ntype ServerSearchCapableProvider = (typeof SERVER_SEARCH_PROVIDERS)[number];\ntype DetectedProvider = ServerSearchCapableProvider | \"other\";\n\ntype ServerToolSearchTool = { type: string; name?: string };\n\nconst SERVER_TOOL_SEARCH_TOOLS = {\n anthropic: {\n type: \"tool_search_tool_bm25_20251119\",\n name: \"tool_search_tool_bm25\",\n },\n openai: { type: \"tool_search\" },\n} as const satisfies Record<ServerSearchCapableProvider, ServerToolSearchTool>;\n\n/**\n * Provider-side tool search middleware.\n *\n * Leverages server-side tool search: the full client tool catalog is forwarded\n * to the provider, with deferred tools marked `defer_loading` so the provider\n * discloses them on demand via its own search. A tool is deferred when it is\n * named in `searchableTools` or built with `extras.defer_loading: true`.\n *\n * Requires a model with server-side tool search support: OpenAI gpt-5.4+ or Anthropic\n * Claude Sonnet 4+/Opus 4+/Haiku 4.5+. Non-Anthropic/OpenAI providers throw; an\n * in-family model that is too old surfaces the provider's own API error rather\n * than being gated here.\n *\n * @example\n * ```ts\n * import { createAgent, providerToolSearchMiddleware } from \"langchain\";\n * import { ChatAnthropic } from \"@langchain/anthropic\";\n *\n * const agent = createAgent({\n * model: new ChatAnthropic({ model: \"claude-sonnet-4-5\" }),\n * tools: [getWeather, ...nicheTools],\n * middleware: [\n * // Defer the niche tools behind the provider's tool search; the model\n * // discovers them on demand instead of receiving every schema up front.\n * providerToolSearchMiddleware({ searchableTools: nicheTools }),\n * ],\n * });\n * ```\n *\n * @example\n * ```ts\n * import { tool } from \"@langchain/core/tools\";\n * import { createAgent, providerToolSearchMiddleware } from \"langchain\";\n * import { ChatAnthropic } from \"@langchain/anthropic\";\n *\n * // A tool marked `defer_loading` at construction is deferred on its own —\n * // no need to list it in `searchableTools`; the middleware honors the flag.\n * const sendEmail = tool(sendEmailFn, {\n * name: \"send_email\",\n * description: \"Send an email\",\n * schema: sendEmailSchema,\n * extras: { defer_loading: true },\n * });\n *\n * const agent = createAgent({\n * model: new ChatAnthropic({ model: \"claude-sonnet-4-5\" }),\n * tools: [getWeather, sendEmail],\n * middleware: [providerToolSearchMiddleware()],\n * });\n * ```\n *\n * @param config - Configuration options for the middleware\n * @param config.searchableTools - Tools to defer behind tool search\n * @returns A middleware instance that can be used with `createAgent`\n */\nexport function providerToolSearchMiddleware(\n config: ProviderToolSearchMiddlewareConfig = {}\n) {\n const deferNames = toToolNames(config.searchableTools);\n\n return createMiddleware({\n name: \"ProviderToolSearch\",\n wrapModelCall: (request, handler) => {\n const tools = request.tools ?? [];\n\n // Fail fast if we try to defer a tool that is not bound to the model\n if (deferNames.size > 0) {\n const available = tools.filter(isLangChainTool).map((t) => t.name);\n const unknown = [...deferNames].filter(\n (name) => !available.includes(name)\n );\n if (unknown.length > 0) {\n throw new Error(\n `providerToolSearchMiddleware: searchableTools references tool(s) not bound to the model: ${unknown.join(\", \")}`\n );\n }\n }\n\n const provider = getModelProvider(request.model);\n if (!supportsProviderToolSearch(provider)) {\n throw new Error(\n `providerToolSearchMiddleware requires a provider with server-side tool search, but got ${provider}`\n );\n }\n\n // Nothing to defer -> pass thru\n if (!hasDeferredTools(tools, deferNames)) return handler(request);\n\n // For each deferred tool, emit a minimal binding spec carrying `defer_loading`.\n const boundTools = tools.map((tool) =>\n deferToolIfNeeded(tool, deferNames)\n );\n\n const nativeSearchTool = SERVER_TOOL_SEARCH_TOOLS[provider];\n return handler({ ...request, tools: [...boundTools, nativeSearchTool] });\n },\n });\n}\n\nfunction isDeferred(\n tool: unknown,\n deferNames: Set<string>\n): tool is StructuredToolParams {\n return (\n isLangChainTool(tool) &&\n (tool.extras?.defer_loading === true || deferNames.has(tool.name))\n );\n}\n\nfunction hasDeferredTools(\n tools: readonly (ClientTool | ServerTool)[],\n deferNames: Set<string>\n): boolean {\n return tools.some((tool) => isDeferred(tool, deferNames));\n}\n\n/**\n * If a tool should be deferred, return a minimal binding spec carrying `defer_loading`.\n * Otherwise return it as-is\n */\nfunction deferToolIfNeeded(\n tool: ClientTool | ServerTool,\n deferNames: Set<string>\n): ClientTool | ServerTool {\n if (!isDeferred(tool, deferNames)) return tool;\n return {\n name: tool.name,\n description: tool.description,\n schema: tool.schema,\n extras: { ...tool.extras, defer_loading: true },\n };\n}\n\n/** Flatten a list of tool names/instances into a set of tool names. */\nfunction toToolNames(tools: ToolIdentifier[] = []): Set<string> {\n return new Set(tools.map((t) => (typeof t === \"string\" ? t : t.name)));\n}\n\nfunction getModelProvider(model: LanguageModelLike): DetectedProvider {\n const name = model.getName();\n const configured =\n name === \"ConfigurableModel\"\n ? (model as ConfigurableModel)._defaultConfig?.modelProvider\n : undefined;\n if (name === \"ChatAnthropic\" || configured === \"anthropic\")\n return \"anthropic\";\n if (name === \"ChatOpenAI\" || configured === \"openai\") return \"openai\";\n return \"other\";\n}\n\nfunction supportsProviderToolSearch(\n provider: DetectedProvider\n): provider is ServerSearchCapableProvider {\n return (SERVER_SEARCH_PROVIDERS as readonly string[]).includes(provider);\n}\n"],"mappings":";;;AA4BA,MAAM,0BAA0B,CAC9B,aACA,QACF;AAOA,MAAM,2BAA2B;CAC/B,WAAW;EACT,MAAM;EACN,MAAM;CACR;CACA,QAAQ,EAAE,MAAM,cAAc;AAChC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,6BACd,SAA6C,CAAC,GAC9C;CACA,MAAM,aAAa,YAAY,OAAO,eAAe;CAErD,OAAOA,mBAAAA,iBAAiB;EACtB,MAAM;EACN,gBAAgB,SAAS,YAAY;GACnC,MAAM,QAAQ,QAAQ,SAAS,CAAC;GAGhC,IAAI,WAAW,OAAO,GAAG;IACvB,MAAM,YAAY,MAAM,OAAOC,uCAAAA,eAAe,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI;IACjE,MAAM,UAAU,CAAC,GAAG,UAAU,CAAC,CAAC,QAC7B,SAAS,CAAC,UAAU,SAAS,IAAI,CACpC;IACA,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,MACR,4FAA4F,QAAQ,KAAK,IAAI,GAC/G;GAEJ;GAEA,MAAM,WAAW,iBAAiB,QAAQ,KAAK;GAC/C,IAAI,CAAC,2BAA2B,QAAQ,GACtC,MAAM,IAAI,MACR,0FAA0F,UAC5F;GAIF,IAAI,CAAC,iBAAiB,OAAO,UAAU,GAAG,OAAO,QAAQ,OAAO;GAGhE,MAAM,aAAa,MAAM,KAAK,SAC5B,kBAAkB,MAAM,UAAU,CACpC;GAEA,MAAM,mBAAmB,yBAAyB;GAClD,OAAO,QAAQ;IAAE,GAAG;IAAS,OAAO,CAAC,GAAG,YAAY,gBAAgB;GAAE,CAAC;EACzE;CACF,CAAC;AACH;AAEA,SAAS,WACP,MACA,YAC8B;CAC9B,QAAA,GACEA,uCAAAA,gBAAAA,CAAgB,IAAI,MACnB,KAAK,QAAQ,kBAAkB,QAAQ,WAAW,IAAI,KAAK,IAAI;AAEpE;AAEA,SAAS,iBACP,OACA,YACS;CACT,OAAO,MAAM,MAAM,SAAS,WAAW,MAAM,UAAU,CAAC;AAC1D;;;;;AAMA,SAAS,kBACP,MACA,YACyB;CACzB,IAAI,CAAC,WAAW,MAAM,UAAU,GAAG,OAAO;CAC1C,OAAO;EACL,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,QAAQ,KAAK;EACb,QAAQ;GAAE,GAAG,KAAK;GAAQ,eAAe;EAAK;CAChD;AACF;;AAGA,SAAS,YAAY,QAA0B,CAAC,GAAgB;CAC9D,OAAO,IAAI,IAAI,MAAM,KAAK,MAAO,OAAO,MAAM,WAAW,IAAI,EAAE,IAAK,CAAC;AACvE;AAEA,SAAS,iBAAiB,OAA4C;CACpE,MAAM,OAAO,MAAM,QAAQ;CAC3B,MAAM,aACJ,SAAS,sBACJ,MAA4B,gBAAgB,gBAC7C,KAAA;CACN,IAAI,SAAS,mBAAmB,eAAe,aAC7C,OAAO;CACT,IAAI,SAAS,gBAAgB,eAAe,UAAU,OAAO;CAC7D,OAAO;AACT;AAEA,SAAS,2BACP,UACyC;CACzC,OAAQ,wBAA8C,SAAS,QAAQ;AACzE"}
|
|
@@ -259,8 +259,9 @@ model: zod_v3.z.custom().optional() }),
|
|
|
259
259
|
const cutoffIndex = await determineCutoffIndex(conversationMessages, validatedKeep, tokenCounter, model);
|
|
260
260
|
if (cutoffIndex <= 0) return;
|
|
261
261
|
const { messagesToSummarize, preservedMessages } = partitionMessages(systemPrompt, conversationMessages, cutoffIndex);
|
|
262
|
+
const summary = await createSummary(messagesToSummarize, model, summaryPrompt, tokenCounter, trimTokensToSummarize, runtime);
|
|
262
263
|
const summaryMessage = new _langchain_core_messages.HumanMessage({
|
|
263
|
-
content: `${summaryPrefix}\n\n${
|
|
264
|
+
content: `${summaryPrefix}\n\n${summary}`,
|
|
264
265
|
id: conversationMessages[0].id,
|
|
265
266
|
additional_kwargs: { lc_source: "summarization" }
|
|
266
267
|
});
|
|
@@ -534,7 +535,13 @@ async function createSummary(messagesToSummarize, model, summaryPrompt, tokenCou
|
|
|
534
535
|
const formattedMessages = (0, _langchain_core_messages.getBufferString)(trimmedMessages);
|
|
535
536
|
try {
|
|
536
537
|
const formattedPrompt = summaryPrompt.replace("{messages}", formattedMessages);
|
|
537
|
-
|
|
538
|
+
/**
|
|
539
|
+
* Merge parent runnable config with summarization metadata so LangGraph's
|
|
540
|
+
* stream handlers (and other callback-based consumers) can properly track
|
|
541
|
+
* and tag the summarization model call.
|
|
542
|
+
*/
|
|
543
|
+
const baseConfig = (0, _langchain_core_runnables.pickRunnableConfigKeys)(runtime) ?? {};
|
|
544
|
+
const config = (0, _langchain_core_runnables.mergeConfigs)(baseConfig, {
|
|
538
545
|
metadata: { lc_source: "summarization" },
|
|
539
546
|
tags: [require_constants.INTERNAL_CALL_TAG]
|
|
540
547
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"summarization.cjs","names":["z","z4","createMiddleware","initChatModel","countTokensApproximately","HumanMessage","RemoveMessage","REMOVE_ALL_MESSAGES","SystemMessage","AIMessage","hasToolCalls","ToolMessage","INTERNAL_CALL_TAG"],"sources":["../../../src/agents/middleware/summarization.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { z as z4 } from \"zod/v4\";\nimport { v4 as uuid } from \"@langchain/core/utils/uuid\";\nimport {\n BaseMessage,\n AIMessage,\n SystemMessage,\n ToolMessage,\n RemoveMessage,\n trimMessages,\n HumanMessage,\n getBufferString,\n} from \"@langchain/core/messages\";\nimport {\n BaseLanguageModel,\n getModelContextSize,\n} from \"@langchain/core/language_models/base\";\nimport {\n interopSafeParse,\n InferInteropZodInput,\n InferInteropZodOutput,\n} from \"@langchain/core/utils/types\";\nimport {\n mergeConfigs,\n pickRunnableConfigKeys,\n type RunnableConfig,\n} from \"@langchain/core/runnables\";\nimport { REMOVE_ALL_MESSAGES } from \"@langchain/langgraph\";\nimport { createMiddleware } from \"../middleware.js\";\nimport { INTERNAL_CALL_TAG } from \"./constants.js\";\nimport { countTokensApproximately } from \"./utils.js\";\nimport { hasToolCalls } from \"../utils.js\";\nimport { initChatModel } from \"../../chat_models/universal.js\";\nimport type { Runtime } from \"../runtime.js\";\n\nexport const DEFAULT_SUMMARY_PROMPT = `<role>\nContext Extraction Assistant\n</role>\n\n<primary_objective>\nYour sole objective in this task is to extract the highest quality/most relevant context from the conversation history below.\n</primary_objective>\n\n<objective_information>\nYou're nearing the total number of input tokens you can accept, so you must extract the highest quality/most relevant pieces of information from your conversation history.\nThis context will then overwrite the conversation history presented below. Because of this, ensure the context you extract is only the most important information to your overall goal.\n</objective_information>\n\n<instructions>\nThe conversation history below will be replaced with the context you extract in this step. Because of this, you must do your very best to extract and record all of the most important context from the conversation history.\nYou want to ensure that you don't repeat any actions you've already completed, so the context you extract from the conversation history should be focused on the most important information to your overall goal.\n</instructions>\n\nThe user will message you with the full message history you'll be extracting context from, to then replace. Carefully read over it all, and think deeply about what information is most important to your overall goal that should be saved:\n\nWith all of this in mind, please carefully read over the entire conversation history, and extract the most important and relevant context to replace it so that you can free up space in the conversation history.\nRespond ONLY with the extracted context. Do not include any additional information, or text before or after the extracted context.\n\n<messages>\nMessages to summarize:\n{messages}\n</messages>`;\n\nconst DEFAULT_SUMMARY_PREFIX = \"Here is a summary of the conversation to date:\";\nconst DEFAULT_MESSAGES_TO_KEEP = 20;\nconst DEFAULT_TRIM_TOKEN_LIMIT = 4000;\nconst DEFAULT_FALLBACK_MESSAGE_COUNT = 15;\nconst SEARCH_RANGE_FOR_TOOL_PAIRS = 5;\n\nconst tokenCounterSchema = z\n .function()\n .args(z.array(z.custom<BaseMessage>()))\n .returns(z.union([z.number(), z.promise(z.number())]));\nexport type TokenCounter = (\n messages: BaseMessage[]\n) => number | Promise<number>;\n\nexport const contextSizeSchema = z\n .object({\n /**\n * Fraction of the model's context size to use as the trigger\n */\n fraction: z\n .number()\n .gt(0, \"Fraction must be greater than 0\")\n .max(1, \"Fraction must be less than or equal to 1\")\n .optional(),\n /**\n * Number of tokens to use as the trigger\n */\n tokens: z.number().positive(\"Tokens must be greater than 0\").optional(),\n /**\n * Number of messages to use as the trigger\n */\n messages: z\n .number()\n .int(\"Messages must be an integer\")\n .positive(\"Messages must be greater than 0\")\n .optional(),\n })\n .refine(\n (data) => {\n const count = [data.fraction, data.tokens, data.messages].filter(\n (v) => v !== undefined\n ).length;\n return count >= 1;\n },\n {\n message: \"At least one of fraction, tokens, or messages must be provided\",\n }\n );\nexport type ContextSize = z.infer<typeof contextSizeSchema>;\n\nexport const keepSchema = z\n .object({\n /**\n * Fraction of the model's context size to keep\n */\n fraction: z\n .number()\n .min(0, \"Messages must be non-negative\")\n .max(1, \"Fraction must be less than or equal to 1\")\n .optional(),\n /**\n * Number of tokens to keep\n */\n tokens: z\n .number()\n .min(0, \"Tokens must be greater than or equal to 0\")\n .optional(),\n messages: z\n .number()\n .int(\"Messages must be an integer\")\n .min(0, \"Messages must be non-negative\")\n .optional(),\n })\n .refine(\n (data) => {\n const count = [data.fraction, data.tokens, data.messages].filter(\n (v) => v !== undefined\n ).length;\n return count === 1;\n },\n {\n message: \"Exactly one of fraction, tokens, or messages must be provided\",\n }\n );\nexport type KeepSize = z.infer<typeof keepSchema>;\n\nconst contextSchema = z.object({\n /**\n * Model to use for summarization\n */\n model: z.custom<string | BaseLanguageModel>(),\n /**\n * Trigger conditions for summarization.\n * Can be a single condition object (all properties must be met) or an array of conditions (any condition must be met).\n *\n * @example\n * ```ts\n * // Single condition: trigger if tokens >= 5000 AND messages >= 3\n * trigger: { tokens: 5000, messages: 3 }\n *\n * // Multiple conditions: trigger if (tokens >= 5000 AND messages >= 3) OR (tokens >= 3000 AND messages >= 6)\n * trigger: [\n * { tokens: 5000, messages: 3 },\n * { tokens: 3000, messages: 6 }\n * ]\n * ```\n */\n trigger: z.union([contextSizeSchema, z.array(contextSizeSchema)]).optional(),\n /**\n * Keep conditions for summarization\n */\n keep: keepSchema.optional(),\n /**\n * Token counter function to use for summarization\n */\n tokenCounter: tokenCounterSchema.optional(),\n /**\n * Summary prompt to use for summarization\n * @default {@link DEFAULT_SUMMARY_PROMPT}\n */\n summaryPrompt: z.string().default(DEFAULT_SUMMARY_PROMPT),\n /**\n * Number of tokens to trim to before summarizing\n */\n trimTokensToSummarize: z.number().optional(),\n /**\n * Prefix to add to the summary\n */\n summaryPrefix: z.string().optional(),\n /**\n * @deprecated Use `trigger: { tokens: value }` instead.\n */\n maxTokensBeforeSummary: z.number().optional(),\n /**\n * @deprecated Use `keep: { messages: value }` instead.\n */\n messagesToKeep: z.number().optional(),\n});\n\nexport type SummarizationMiddlewareConfig = InferInteropZodInput<\n typeof contextSchema\n>;\n\n/**\n * Get max input tokens from model profile or fallback to model name lookup\n */\nexport function getProfileLimits(input: BaseLanguageModel): number | undefined {\n // Access maxInputTokens on the model profile directly if available\n if (\n \"profile\" in input &&\n typeof input.profile === \"object\" &&\n input.profile &&\n \"maxInputTokens\" in input.profile &&\n (typeof input.profile.maxInputTokens === \"number\" ||\n input.profile.maxInputTokens == null)\n ) {\n return input.profile.maxInputTokens ?? undefined;\n }\n\n // Fallback to using model name if available\n if (\"model\" in input && typeof input.model === \"string\") {\n return getModelContextSize(input.model);\n }\n if (\"modelName\" in input && typeof input.modelName === \"string\") {\n return getModelContextSize(input.modelName);\n }\n\n return undefined;\n}\n\n/**\n * Summarization middleware that automatically summarizes conversation history when token limits are approached.\n *\n * This middleware monitors message token counts and automatically summarizes older\n * messages when a threshold is reached, preserving recent messages and maintaining\n * context continuity by ensuring AI/Tool message pairs remain together.\n *\n * @param options Configuration options for the summarization middleware\n * @returns A middleware instance\n *\n * @example\n * ```ts\n * import { summarizationMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * // Single condition: trigger if tokens >= 4000 AND messages >= 10\n * const agent1 = createAgent({\n * llm: model,\n * tools: [getWeather],\n * middleware: [\n * summarizationMiddleware({\n * model: new ChatOpenAI({ model: \"gpt-4o\" }),\n * trigger: { tokens: 4000, messages: 10 },\n * keep: { messages: 20 },\n * })\n * ],\n * });\n *\n * // Multiple conditions: trigger if (tokens >= 5000 AND messages >= 3) OR (tokens >= 3000 AND messages >= 6)\n * const agent2 = createAgent({\n * llm: model,\n * tools: [getWeather],\n * middleware: [\n * summarizationMiddleware({\n * model: new ChatOpenAI({ model: \"gpt-4o\" }),\n * trigger: [\n * { tokens: 5000, messages: 3 },\n * { tokens: 3000, messages: 6 },\n * ],\n * keep: { messages: 20 },\n * })\n * ],\n * });\n *\n * ```\n */\nexport function summarizationMiddleware(\n options: SummarizationMiddlewareConfig\n) {\n /**\n * Parse user options to get their explicit values\n */\n const { data: userOptions, error } = interopSafeParse(contextSchema, options);\n if (error) {\n throw new Error(\n `Invalid summarization middleware options: ${z4.prettifyError(error)}`\n );\n }\n\n return createMiddleware({\n name: \"SummarizationMiddleware\",\n contextSchema: contextSchema.extend({\n /**\n * `model` should be required when initializing the middleware,\n * but can be omitted within context when invoking the middleware.\n */\n model: z.custom<BaseLanguageModel>().optional(),\n }),\n beforeModel: async (state, runtime) => {\n let trigger: ContextSize | ContextSize[] | undefined =\n userOptions.trigger;\n let keep: ContextSize = userOptions.keep as InferInteropZodOutput<\n typeof keepSchema\n >;\n\n /**\n * Handle deprecated parameters\n */\n if (userOptions.maxTokensBeforeSummary !== undefined) {\n console.warn(\n \"maxTokensBeforeSummary is deprecated. Use `trigger: { tokens: value }` instead.\"\n );\n if (trigger === undefined) {\n trigger = { tokens: userOptions.maxTokensBeforeSummary };\n }\n }\n\n /**\n * Handle deprecated parameters\n */\n if (userOptions.messagesToKeep !== undefined) {\n console.warn(\n \"messagesToKeep is deprecated. Use `keep: { messages: value }` instead.\"\n );\n if (\n !keep ||\n (keep &&\n \"messages\" in keep &&\n keep.messages === DEFAULT_MESSAGES_TO_KEEP)\n ) {\n keep = { messages: userOptions.messagesToKeep };\n }\n }\n\n /**\n * Merge context with user options\n */\n const resolvedTrigger =\n runtime.context?.trigger !== undefined\n ? runtime.context.trigger\n : trigger;\n const resolvedKeep =\n runtime.context?.keep !== undefined\n ? runtime.context.keep\n : (keep ?? { messages: DEFAULT_MESSAGES_TO_KEEP });\n\n const validatedKeep = keepSchema.parse(resolvedKeep);\n\n /**\n * Validate trigger conditions\n */\n let triggerConditions: ContextSize[] = [];\n if (resolvedTrigger === undefined) {\n triggerConditions = [];\n } else if (Array.isArray(resolvedTrigger)) {\n /**\n * It's an array of ContextSize objects\n */\n triggerConditions = (resolvedTrigger as ContextSize[]).map((t) =>\n contextSizeSchema.parse(t)\n );\n } else {\n /**\n * Single ContextSize object - all properties must be satisfied (AND logic)\n */\n triggerConditions = [contextSizeSchema.parse(resolvedTrigger)];\n }\n\n /**\n * Check if profile is required\n */\n const requiresProfile =\n triggerConditions.some((c) => \"fraction\" in c) ||\n \"fraction\" in validatedKeep;\n\n const model =\n typeof userOptions.model === \"string\"\n ? await initChatModel(userOptions.model)\n : userOptions.model;\n\n if (requiresProfile && !getProfileLimits(model)) {\n throw new Error(\n \"Model profile information is required to use fractional token limits. \" +\n \"Use absolute token counts instead.\"\n );\n }\n\n const summaryPrompt =\n runtime.context?.summaryPrompt === DEFAULT_SUMMARY_PROMPT\n ? (userOptions.summaryPrompt ?? DEFAULT_SUMMARY_PROMPT)\n : (runtime.context?.summaryPrompt ??\n userOptions.summaryPrompt ??\n DEFAULT_SUMMARY_PROMPT);\n const summaryPrefix =\n runtime.context.summaryPrefix ??\n userOptions.summaryPrefix ??\n DEFAULT_SUMMARY_PREFIX;\n const trimTokensToSummarize =\n runtime.context?.trimTokensToSummarize !== undefined\n ? runtime.context.trimTokensToSummarize\n : (userOptions.trimTokensToSummarize ?? DEFAULT_TRIM_TOKEN_LIMIT);\n\n /**\n * Ensure all messages have IDs\n */\n ensureMessageIds(state.messages);\n\n const tokenCounter =\n runtime.context?.tokenCounter !== undefined\n ? runtime.context.tokenCounter\n : (userOptions.tokenCounter ?? countTokensApproximately);\n const totalTokens = await tokenCounter(state.messages);\n const doSummarize = await shouldSummarize(\n state.messages,\n totalTokens,\n triggerConditions,\n model\n );\n\n if (!doSummarize) {\n return;\n }\n\n const { systemPrompt, conversationMessages } = splitSystemMessage(\n state.messages\n );\n const cutoffIndex = await determineCutoffIndex(\n conversationMessages,\n validatedKeep,\n tokenCounter,\n model\n );\n\n if (cutoffIndex <= 0) {\n return;\n }\n\n const { messagesToSummarize, preservedMessages } = partitionMessages(\n systemPrompt,\n conversationMessages,\n cutoffIndex\n );\n\n const summary = await createSummary(\n messagesToSummarize,\n model,\n summaryPrompt,\n tokenCounter,\n trimTokensToSummarize,\n runtime\n );\n\n // Reuse a replaced message's id so the summary projects as an update, not a new message.\n const summaryMessage = new HumanMessage({\n content: `${summaryPrefix}\\n\\n${summary}`,\n id: conversationMessages[0].id,\n additional_kwargs: { lc_source: \"summarization\" },\n });\n\n return {\n messages: [\n new RemoveMessage({ id: REMOVE_ALL_MESSAGES }),\n summaryMessage,\n ...preservedMessages,\n ],\n };\n },\n });\n}\n\n/**\n * Ensure all messages have unique IDs\n */\nfunction ensureMessageIds(messages: BaseMessage[]): void {\n for (const msg of messages) {\n if (!msg.id) {\n msg.id = uuid();\n }\n }\n}\n\n/**\n * Separate system message from conversation messages\n */\nfunction splitSystemMessage(messages: BaseMessage[]): {\n systemPrompt?: SystemMessage;\n conversationMessages: BaseMessage[];\n} {\n if (messages.length > 0 && SystemMessage.isInstance(messages[0])) {\n return {\n systemPrompt: messages[0] as SystemMessage,\n conversationMessages: messages.slice(1),\n };\n }\n return {\n conversationMessages: messages,\n };\n}\n\n/**\n * Partition messages into those to summarize and those to preserve\n */\nfunction partitionMessages(\n systemPrompt: SystemMessage | undefined,\n conversationMessages: BaseMessage[],\n cutoffIndex: number\n): { messagesToSummarize: BaseMessage[]; preservedMessages: BaseMessage[] } {\n const messagesToSummarize = conversationMessages.slice(0, cutoffIndex);\n const preservedMessages = conversationMessages.slice(cutoffIndex);\n\n // Include system message in messages to summarize to capture previous summaries\n if (systemPrompt) {\n messagesToSummarize.unshift(systemPrompt);\n }\n\n return { messagesToSummarize, preservedMessages };\n}\n\n/**\n * Determine whether summarization should run for the current token usage\n *\n * @param messages - Current messages in the conversation\n * @param totalTokens - Total token count for all messages\n * @param triggerConditions - Array of trigger conditions. Returns true if ANY condition is satisfied (OR logic).\n * Within each condition, ALL specified properties must be satisfied (AND logic).\n * @param model - The language model being used\n * @returns true if summarization should be triggered\n */\nasync function shouldSummarize(\n messages: BaseMessage[],\n totalTokens: number,\n triggerConditions: ContextSize[],\n model: BaseLanguageModel\n): Promise<boolean> {\n if (triggerConditions.length === 0) {\n return false;\n }\n\n /**\n * Check each condition (OR logic between conditions)\n */\n for (const trigger of triggerConditions) {\n /**\n * Within a single condition, all specified properties must be satisfied (AND logic)\n */\n let conditionMet = true;\n let hasAnyProperty = false;\n\n if (trigger.messages !== undefined) {\n hasAnyProperty = true;\n if (messages.length < trigger.messages) {\n conditionMet = false;\n }\n }\n\n if (trigger.tokens !== undefined) {\n hasAnyProperty = true;\n if (totalTokens < trigger.tokens) {\n conditionMet = false;\n }\n }\n\n if (trigger.fraction !== undefined) {\n hasAnyProperty = true;\n const maxInputTokens = getProfileLimits(model);\n if (typeof maxInputTokens === \"number\") {\n const threshold = Math.floor(maxInputTokens * trigger.fraction);\n if (totalTokens < threshold) {\n conditionMet = false;\n }\n } else {\n /**\n * If fraction is specified but we can't get model limits, skip this condition\n */\n conditionMet = false;\n }\n }\n\n /**\n * If condition has at least one property and all properties are satisfied, trigger summarization\n */\n if (hasAnyProperty && conditionMet) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Determine cutoff index respecting retention configuration\n */\nasync function determineCutoffIndex(\n messages: BaseMessage[],\n keep: ContextSize,\n tokenCounter: TokenCounter,\n model: BaseLanguageModel\n): Promise<number> {\n if (\"tokens\" in keep || \"fraction\" in keep) {\n const tokenBasedCutoff = await findTokenBasedCutoff(\n messages,\n keep,\n tokenCounter,\n model\n );\n if (typeof tokenBasedCutoff === \"number\") {\n return tokenBasedCutoff;\n }\n /**\n * Fallback to message count if token-based fails\n */\n return findSafeCutoff(messages, DEFAULT_MESSAGES_TO_KEEP);\n }\n /**\n * find cutoff index based on message count\n */\n return findSafeCutoff(messages, keep.messages ?? DEFAULT_MESSAGES_TO_KEEP);\n}\n\n/**\n * Find cutoff index based on target token retention\n */\nasync function findTokenBasedCutoff(\n messages: BaseMessage[],\n keep: ContextSize,\n tokenCounter: TokenCounter,\n model: BaseLanguageModel\n): Promise<number | undefined> {\n if (messages.length === 0) {\n return 0;\n }\n\n let targetTokenCount: number;\n\n if (\"fraction\" in keep && keep.fraction !== undefined) {\n const maxInputTokens = getProfileLimits(model);\n if (typeof maxInputTokens !== \"number\") {\n return;\n }\n targetTokenCount = Math.floor(maxInputTokens * keep.fraction);\n } else if (\"tokens\" in keep && keep.tokens !== undefined) {\n targetTokenCount = Math.floor(keep.tokens);\n } else {\n return;\n }\n\n if (targetTokenCount <= 0) {\n targetTokenCount = 1;\n }\n\n const totalTokens = await tokenCounter(messages);\n if (totalTokens <= targetTokenCount) {\n return 0;\n }\n\n /**\n * Use binary search to identify the earliest message index that keeps the\n * suffix within the token budget.\n */\n let left = 0;\n let right = messages.length;\n let cutoffCandidate = messages.length;\n const maxIterations = Math.floor(Math.log2(messages.length)) + 1;\n\n for (let i = 0; i < maxIterations; i++) {\n if (left >= right) {\n break;\n }\n\n const mid = Math.floor((left + right) / 2);\n const suffixTokens = await tokenCounter(messages.slice(mid));\n if (suffixTokens <= targetTokenCount) {\n cutoffCandidate = mid;\n right = mid;\n } else {\n left = mid + 1;\n }\n }\n\n if (cutoffCandidate === messages.length) {\n cutoffCandidate = left;\n }\n\n if (cutoffCandidate >= messages.length) {\n if (messages.length === 1) {\n return 0;\n }\n cutoffCandidate = messages.length - 1;\n }\n\n /**\n * Find safe cutoff point that preserves AI/Tool pairs.\n * If cutoff lands on ToolMessage, move backward to include the AIMessage.\n */\n const safeCutoff = findSafeCutoffPoint(messages, cutoffCandidate);\n\n /**\n * If findSafeCutoffPoint moved forward (fallback case), verify it's safe.\n * If it moved backward, we already have a safe point.\n */\n if (safeCutoff <= cutoffCandidate) {\n return safeCutoff;\n }\n\n /**\n * Fallback: iterate backward to find a safe cutoff\n */\n for (let i = cutoffCandidate; i >= 0; i--) {\n if (isSafeCutoffPoint(messages, i)) {\n return i;\n }\n }\n\n return 0;\n}\n\n/**\n * Find safe cutoff point that preserves AI/Tool message pairs\n */\nfunction findSafeCutoff(\n messages: BaseMessage[],\n messagesToKeep: number\n): number {\n if (messages.length <= messagesToKeep) {\n return 0;\n }\n\n const targetCutoff = messages.length - messagesToKeep;\n\n /**\n * First, try to find a safe cutoff point using findSafeCutoffPoint.\n * This handles the case where cutoff lands on a ToolMessage by moving\n * backward to include the corresponding AIMessage.\n */\n const safeCutoff = findSafeCutoffPoint(messages, targetCutoff);\n\n /**\n * If findSafeCutoffPoint moved backward (found matching AIMessage), use it.\n */\n if (safeCutoff <= targetCutoff) {\n return safeCutoff;\n }\n\n /**\n * Fallback: iterate backward to find a safe cutoff\n */\n for (let i = targetCutoff; i >= 0; i--) {\n if (isSafeCutoffPoint(messages, i)) {\n return i;\n }\n }\n\n return 0;\n}\n\n/**\n * Check if cutting at index would separate AI/Tool message pairs\n */\nfunction isSafeCutoffPoint(\n messages: BaseMessage[],\n cutoffIndex: number\n): boolean {\n if (cutoffIndex >= messages.length) {\n return true;\n }\n\n /**\n * Prevent preserved messages from starting with AI message containing tool calls\n */\n if (\n cutoffIndex < messages.length &&\n AIMessage.isInstance(messages[cutoffIndex]) &&\n hasToolCalls(messages[cutoffIndex])\n ) {\n return false;\n }\n\n const searchStart = Math.max(0, cutoffIndex - SEARCH_RANGE_FOR_TOOL_PAIRS);\n const searchEnd = Math.min(\n messages.length,\n cutoffIndex + SEARCH_RANGE_FOR_TOOL_PAIRS\n );\n\n for (let i = searchStart; i < searchEnd; i++) {\n if (!hasToolCalls(messages[i])) {\n continue;\n }\n\n const toolCallIds = extractToolCallIds(messages[i] as AIMessage);\n if (cutoffSeparatesToolPair(messages, i, cutoffIndex, toolCallIds)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Extract tool call IDs from an AI message\n */\nfunction extractToolCallIds(aiMessage: AIMessage): Set<string> {\n const toolCallIds = new Set<string>();\n if (aiMessage.tool_calls) {\n for (const toolCall of aiMessage.tool_calls) {\n const id =\n typeof toolCall === \"object\" && \"id\" in toolCall ? toolCall.id : null;\n if (id) {\n toolCallIds.add(id);\n }\n }\n }\n return toolCallIds;\n}\n\n/**\n * Find a safe cutoff point that doesn't split AI/Tool message pairs.\n *\n * If the message at `cutoffIndex` is a `ToolMessage`, search backward for the\n * `AIMessage` containing the corresponding `tool_calls` and adjust the cutoff to\n * include it. This ensures tool call requests and responses stay together.\n *\n * Falls back to advancing forward past `ToolMessage` objects only if no matching\n * `AIMessage` is found (edge case).\n */\nfunction findSafeCutoffPoint(\n messages: BaseMessage[],\n cutoffIndex: number\n): number {\n if (\n cutoffIndex >= messages.length ||\n !ToolMessage.isInstance(messages[cutoffIndex])\n ) {\n return cutoffIndex;\n }\n\n // Collect tool_call_ids from consecutive ToolMessages at/after cutoff\n const toolCallIds = new Set<string>();\n let idx = cutoffIndex;\n while (idx < messages.length && ToolMessage.isInstance(messages[idx])) {\n const toolMsg = messages[idx] as ToolMessage;\n if (toolMsg.tool_call_id) {\n toolCallIds.add(toolMsg.tool_call_id);\n }\n idx++;\n }\n\n // Search backward for AIMessage with matching tool_calls\n for (let i = cutoffIndex - 1; i >= 0; i--) {\n const msg = messages[i];\n if (AIMessage.isInstance(msg) && hasToolCalls(msg)) {\n const aiToolCallIds = extractToolCallIds(msg as AIMessage);\n // Check if there's any overlap between the tool_call_ids\n for (const id of toolCallIds) {\n if (aiToolCallIds.has(id)) {\n // Found the AIMessage - move cutoff to include it\n return i;\n }\n }\n }\n }\n\n // Fallback: no matching AIMessage found, advance past ToolMessages to avoid\n // orphaned tool responses\n return idx;\n}\n\n/**\n * Check if cutoff separates an AI message from its corresponding tool messages\n */\nfunction cutoffSeparatesToolPair(\n messages: BaseMessage[],\n aiMessageIndex: number,\n cutoffIndex: number,\n toolCallIds: Set<string>\n): boolean {\n for (let j = aiMessageIndex + 1; j < messages.length; j++) {\n const message = messages[j];\n if (\n ToolMessage.isInstance(message) &&\n toolCallIds.has(message.tool_call_id)\n ) {\n const aiBeforeCutoff = aiMessageIndex < cutoffIndex;\n const toolBeforeCutoff = j < cutoffIndex;\n if (aiBeforeCutoff !== toolBeforeCutoff) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Generate summary for the given messages.\n *\n * @param messagesToSummarize - Messages to summarize.\n * @param model - The language model to use for summarization.\n * @param summaryPrompt - The prompt template for summarization.\n * @param tokenCounter - Function to count tokens.\n * @param trimTokensToSummarize - Optional token limit for trimming messages.\n * @param runtime - The runtime environment, used to inherit config so that\n * LangGraph's handlers can properly track and tag the summarization model call.\n */\nasync function createSummary(\n messagesToSummarize: BaseMessage[],\n model: BaseLanguageModel,\n summaryPrompt: string,\n tokenCounter: TokenCounter,\n trimTokensToSummarize: number | undefined,\n runtime: Runtime<unknown>\n): Promise<string> {\n if (!messagesToSummarize.length) {\n return \"No previous conversation history.\";\n }\n\n const trimmedMessages = await trimMessagesForSummary(\n messagesToSummarize,\n tokenCounter,\n trimTokensToSummarize\n );\n\n if (!trimmedMessages.length) {\n return \"Previous conversation was too long to summarize.\";\n }\n\n /**\n * Format messages using getBufferString to avoid token inflation from metadata\n * when str() / JSON.stringify is called on message objects.\n * This produces compact output like:\n * ```\n * Human: What's the weather?\n * AI: Let me check...[tool_calls]\n * Tool: 72°F and sunny\n * ```\n */\n const formattedMessages = getBufferString(trimmedMessages);\n\n try {\n const formattedPrompt = summaryPrompt.replace(\n \"{messages}\",\n formattedMessages\n );\n /**\n * Merge parent runnable config with summarization metadata so LangGraph's\n * stream handlers (and other callback-based consumers) can properly track\n * and tag the summarization model call.\n */\n const baseConfig: RunnableConfig = pickRunnableConfigKeys(runtime) ?? {};\n const config = mergeConfigs(baseConfig, {\n metadata: { lc_source: \"summarization\" },\n tags: [INTERNAL_CALL_TAG],\n });\n const response = await model.invoke(formattedPrompt, config);\n const content = response.content;\n /**\n * Handle both string content and MessageContent array\n */\n if (typeof content === \"string\") {\n return content.trim();\n } else if (Array.isArray(content)) {\n /**\n * Extract text from MessageContent array\n */\n const textContent = content\n .map((item) => {\n if (typeof item === \"string\") return item;\n if (typeof item === \"object\" && item !== null && \"text\" in item) {\n return (item as { text: string }).text;\n }\n return \"\";\n })\n .join(\"\");\n return textContent.trim();\n }\n return \"Error generating summary: Invalid response format\";\n } catch (e) {\n return `Error generating summary: ${e}`;\n }\n}\n\n/**\n * Trim messages to fit within summary generation limits\n */\nasync function trimMessagesForSummary(\n messages: BaseMessage[],\n tokenCounter: TokenCounter,\n trimTokensToSummarize: number | undefined\n): Promise<BaseMessage[]> {\n if (trimTokensToSummarize === undefined) {\n return messages;\n }\n\n try {\n return await trimMessages(messages, {\n maxTokens: trimTokensToSummarize,\n tokenCounter: async (msgs) => tokenCounter(msgs),\n strategy: \"last\",\n allowPartial: true,\n includeSystem: true,\n });\n } catch {\n /**\n * Fallback to last N messages if trimming fails\n */\n return messages.slice(-DEFAULT_FALLBACK_MESSAGE_COUNT);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"summarization.cjs","names":["z","getModelContextSize","interopSafeParse","z4","createMiddleware","initChatModel","countTokensApproximately","HumanMessage","RemoveMessage","REMOVE_ALL_MESSAGES","uuid","SystemMessage","AIMessage","hasToolCalls","ToolMessage","getBufferString","pickRunnableConfigKeys","mergeConfigs","INTERNAL_CALL_TAG","trimMessages"],"sources":["../../../src/agents/middleware/summarization.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { z as z4 } from \"zod/v4\";\nimport { v4 as uuid } from \"@langchain/core/utils/uuid\";\nimport {\n BaseMessage,\n AIMessage,\n SystemMessage,\n ToolMessage,\n RemoveMessage,\n trimMessages,\n HumanMessage,\n getBufferString,\n} from \"@langchain/core/messages\";\nimport {\n BaseLanguageModel,\n getModelContextSize,\n} from \"@langchain/core/language_models/base\";\nimport {\n interopSafeParse,\n InferInteropZodInput,\n InferInteropZodOutput,\n} from \"@langchain/core/utils/types\";\nimport {\n mergeConfigs,\n pickRunnableConfigKeys,\n type RunnableConfig,\n} from \"@langchain/core/runnables\";\nimport { REMOVE_ALL_MESSAGES } from \"@langchain/langgraph\";\nimport { createMiddleware } from \"../middleware.js\";\nimport { INTERNAL_CALL_TAG } from \"./constants.js\";\nimport { countTokensApproximately } from \"./utils.js\";\nimport { hasToolCalls } from \"../utils.js\";\nimport { initChatModel } from \"../../chat_models/universal.js\";\nimport type { Runtime } from \"../runtime.js\";\n\nexport const DEFAULT_SUMMARY_PROMPT = `<role>\nContext Extraction Assistant\n</role>\n\n<primary_objective>\nYour sole objective in this task is to extract the highest quality/most relevant context from the conversation history below.\n</primary_objective>\n\n<objective_information>\nYou're nearing the total number of input tokens you can accept, so you must extract the highest quality/most relevant pieces of information from your conversation history.\nThis context will then overwrite the conversation history presented below. Because of this, ensure the context you extract is only the most important information to your overall goal.\n</objective_information>\n\n<instructions>\nThe conversation history below will be replaced with the context you extract in this step. Because of this, you must do your very best to extract and record all of the most important context from the conversation history.\nYou want to ensure that you don't repeat any actions you've already completed, so the context you extract from the conversation history should be focused on the most important information to your overall goal.\n</instructions>\n\nThe user will message you with the full message history you'll be extracting context from, to then replace. Carefully read over it all, and think deeply about what information is most important to your overall goal that should be saved:\n\nWith all of this in mind, please carefully read over the entire conversation history, and extract the most important and relevant context to replace it so that you can free up space in the conversation history.\nRespond ONLY with the extracted context. Do not include any additional information, or text before or after the extracted context.\n\n<messages>\nMessages to summarize:\n{messages}\n</messages>`;\n\nconst DEFAULT_SUMMARY_PREFIX = \"Here is a summary of the conversation to date:\";\nconst DEFAULT_MESSAGES_TO_KEEP = 20;\nconst DEFAULT_TRIM_TOKEN_LIMIT = 4000;\nconst DEFAULT_FALLBACK_MESSAGE_COUNT = 15;\nconst SEARCH_RANGE_FOR_TOOL_PAIRS = 5;\n\nconst tokenCounterSchema = z\n .function()\n .args(z.array(z.custom<BaseMessage>()))\n .returns(z.union([z.number(), z.promise(z.number())]));\nexport type TokenCounter = (\n messages: BaseMessage[]\n) => number | Promise<number>;\n\nexport const contextSizeSchema = z\n .object({\n /**\n * Fraction of the model's context size to use as the trigger\n */\n fraction: z\n .number()\n .gt(0, \"Fraction must be greater than 0\")\n .max(1, \"Fraction must be less than or equal to 1\")\n .optional(),\n /**\n * Number of tokens to use as the trigger\n */\n tokens: z.number().positive(\"Tokens must be greater than 0\").optional(),\n /**\n * Number of messages to use as the trigger\n */\n messages: z\n .number()\n .int(\"Messages must be an integer\")\n .positive(\"Messages must be greater than 0\")\n .optional(),\n })\n .refine(\n (data) => {\n const count = [data.fraction, data.tokens, data.messages].filter(\n (v) => v !== undefined\n ).length;\n return count >= 1;\n },\n {\n message: \"At least one of fraction, tokens, or messages must be provided\",\n }\n );\nexport type ContextSize = z.infer<typeof contextSizeSchema>;\n\nexport const keepSchema = z\n .object({\n /**\n * Fraction of the model's context size to keep\n */\n fraction: z\n .number()\n .min(0, \"Messages must be non-negative\")\n .max(1, \"Fraction must be less than or equal to 1\")\n .optional(),\n /**\n * Number of tokens to keep\n */\n tokens: z\n .number()\n .min(0, \"Tokens must be greater than or equal to 0\")\n .optional(),\n messages: z\n .number()\n .int(\"Messages must be an integer\")\n .min(0, \"Messages must be non-negative\")\n .optional(),\n })\n .refine(\n (data) => {\n const count = [data.fraction, data.tokens, data.messages].filter(\n (v) => v !== undefined\n ).length;\n return count === 1;\n },\n {\n message: \"Exactly one of fraction, tokens, or messages must be provided\",\n }\n );\nexport type KeepSize = z.infer<typeof keepSchema>;\n\nconst contextSchema = z.object({\n /**\n * Model to use for summarization\n */\n model: z.custom<string | BaseLanguageModel>(),\n /**\n * Trigger conditions for summarization.\n * Can be a single condition object (all properties must be met) or an array of conditions (any condition must be met).\n *\n * @example\n * ```ts\n * // Single condition: trigger if tokens >= 5000 AND messages >= 3\n * trigger: { tokens: 5000, messages: 3 }\n *\n * // Multiple conditions: trigger if (tokens >= 5000 AND messages >= 3) OR (tokens >= 3000 AND messages >= 6)\n * trigger: [\n * { tokens: 5000, messages: 3 },\n * { tokens: 3000, messages: 6 }\n * ]\n * ```\n */\n trigger: z.union([contextSizeSchema, z.array(contextSizeSchema)]).optional(),\n /**\n * Keep conditions for summarization\n */\n keep: keepSchema.optional(),\n /**\n * Token counter function to use for summarization\n */\n tokenCounter: tokenCounterSchema.optional(),\n /**\n * Summary prompt to use for summarization\n * @default {@link DEFAULT_SUMMARY_PROMPT}\n */\n summaryPrompt: z.string().default(DEFAULT_SUMMARY_PROMPT),\n /**\n * Number of tokens to trim to before summarizing\n */\n trimTokensToSummarize: z.number().optional(),\n /**\n * Prefix to add to the summary\n */\n summaryPrefix: z.string().optional(),\n /**\n * @deprecated Use `trigger: { tokens: value }` instead.\n */\n maxTokensBeforeSummary: z.number().optional(),\n /**\n * @deprecated Use `keep: { messages: value }` instead.\n */\n messagesToKeep: z.number().optional(),\n});\n\nexport type SummarizationMiddlewareConfig = InferInteropZodInput<\n typeof contextSchema\n>;\n\n/**\n * Get max input tokens from model profile or fallback to model name lookup\n */\nexport function getProfileLimits(input: BaseLanguageModel): number | undefined {\n // Access maxInputTokens on the model profile directly if available\n if (\n \"profile\" in input &&\n typeof input.profile === \"object\" &&\n input.profile &&\n \"maxInputTokens\" in input.profile &&\n (typeof input.profile.maxInputTokens === \"number\" ||\n input.profile.maxInputTokens == null)\n ) {\n return input.profile.maxInputTokens ?? undefined;\n }\n\n // Fallback to using model name if available\n if (\"model\" in input && typeof input.model === \"string\") {\n return getModelContextSize(input.model);\n }\n if (\"modelName\" in input && typeof input.modelName === \"string\") {\n return getModelContextSize(input.modelName);\n }\n\n return undefined;\n}\n\n/**\n * Summarization middleware that automatically summarizes conversation history when token limits are approached.\n *\n * This middleware monitors message token counts and automatically summarizes older\n * messages when a threshold is reached, preserving recent messages and maintaining\n * context continuity by ensuring AI/Tool message pairs remain together.\n *\n * @param options Configuration options for the summarization middleware\n * @returns A middleware instance\n *\n * @example\n * ```ts\n * import { summarizationMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * // Single condition: trigger if tokens >= 4000 AND messages >= 10\n * const agent1 = createAgent({\n * llm: model,\n * tools: [getWeather],\n * middleware: [\n * summarizationMiddleware({\n * model: new ChatOpenAI({ model: \"gpt-4o\" }),\n * trigger: { tokens: 4000, messages: 10 },\n * keep: { messages: 20 },\n * })\n * ],\n * });\n *\n * // Multiple conditions: trigger if (tokens >= 5000 AND messages >= 3) OR (tokens >= 3000 AND messages >= 6)\n * const agent2 = createAgent({\n * llm: model,\n * tools: [getWeather],\n * middleware: [\n * summarizationMiddleware({\n * model: new ChatOpenAI({ model: \"gpt-4o\" }),\n * trigger: [\n * { tokens: 5000, messages: 3 },\n * { tokens: 3000, messages: 6 },\n * ],\n * keep: { messages: 20 },\n * })\n * ],\n * });\n *\n * ```\n */\nexport function summarizationMiddleware(\n options: SummarizationMiddlewareConfig\n) {\n /**\n * Parse user options to get their explicit values\n */\n const { data: userOptions, error } = interopSafeParse(contextSchema, options);\n if (error) {\n throw new Error(\n `Invalid summarization middleware options: ${z4.prettifyError(error)}`\n );\n }\n\n return createMiddleware({\n name: \"SummarizationMiddleware\",\n contextSchema: contextSchema.extend({\n /**\n * `model` should be required when initializing the middleware,\n * but can be omitted within context when invoking the middleware.\n */\n model: z.custom<BaseLanguageModel>().optional(),\n }),\n beforeModel: async (state, runtime) => {\n let trigger: ContextSize | ContextSize[] | undefined =\n userOptions.trigger;\n let keep: ContextSize = userOptions.keep as InferInteropZodOutput<\n typeof keepSchema\n >;\n\n /**\n * Handle deprecated parameters\n */\n if (userOptions.maxTokensBeforeSummary !== undefined) {\n console.warn(\n \"maxTokensBeforeSummary is deprecated. Use `trigger: { tokens: value }` instead.\"\n );\n if (trigger === undefined) {\n trigger = { tokens: userOptions.maxTokensBeforeSummary };\n }\n }\n\n /**\n * Handle deprecated parameters\n */\n if (userOptions.messagesToKeep !== undefined) {\n console.warn(\n \"messagesToKeep is deprecated. Use `keep: { messages: value }` instead.\"\n );\n if (\n !keep ||\n (keep &&\n \"messages\" in keep &&\n keep.messages === DEFAULT_MESSAGES_TO_KEEP)\n ) {\n keep = { messages: userOptions.messagesToKeep };\n }\n }\n\n /**\n * Merge context with user options\n */\n const resolvedTrigger =\n runtime.context?.trigger !== undefined\n ? runtime.context.trigger\n : trigger;\n const resolvedKeep =\n runtime.context?.keep !== undefined\n ? runtime.context.keep\n : (keep ?? { messages: DEFAULT_MESSAGES_TO_KEEP });\n\n const validatedKeep = keepSchema.parse(resolvedKeep);\n\n /**\n * Validate trigger conditions\n */\n let triggerConditions: ContextSize[] = [];\n if (resolvedTrigger === undefined) {\n triggerConditions = [];\n } else if (Array.isArray(resolvedTrigger)) {\n /**\n * It's an array of ContextSize objects\n */\n triggerConditions = (resolvedTrigger as ContextSize[]).map((t) =>\n contextSizeSchema.parse(t)\n );\n } else {\n /**\n * Single ContextSize object - all properties must be satisfied (AND logic)\n */\n triggerConditions = [contextSizeSchema.parse(resolvedTrigger)];\n }\n\n /**\n * Check if profile is required\n */\n const requiresProfile =\n triggerConditions.some((c) => \"fraction\" in c) ||\n \"fraction\" in validatedKeep;\n\n const model =\n typeof userOptions.model === \"string\"\n ? await initChatModel(userOptions.model)\n : userOptions.model;\n\n if (requiresProfile && !getProfileLimits(model)) {\n throw new Error(\n \"Model profile information is required to use fractional token limits. \" +\n \"Use absolute token counts instead.\"\n );\n }\n\n const summaryPrompt =\n runtime.context?.summaryPrompt === DEFAULT_SUMMARY_PROMPT\n ? (userOptions.summaryPrompt ?? DEFAULT_SUMMARY_PROMPT)\n : (runtime.context?.summaryPrompt ??\n userOptions.summaryPrompt ??\n DEFAULT_SUMMARY_PROMPT);\n const summaryPrefix =\n runtime.context.summaryPrefix ??\n userOptions.summaryPrefix ??\n DEFAULT_SUMMARY_PREFIX;\n const trimTokensToSummarize =\n runtime.context?.trimTokensToSummarize !== undefined\n ? runtime.context.trimTokensToSummarize\n : (userOptions.trimTokensToSummarize ?? DEFAULT_TRIM_TOKEN_LIMIT);\n\n /**\n * Ensure all messages have IDs\n */\n ensureMessageIds(state.messages);\n\n const tokenCounter =\n runtime.context?.tokenCounter !== undefined\n ? runtime.context.tokenCounter\n : (userOptions.tokenCounter ?? countTokensApproximately);\n const totalTokens = await tokenCounter(state.messages);\n const doSummarize = await shouldSummarize(\n state.messages,\n totalTokens,\n triggerConditions,\n model\n );\n\n if (!doSummarize) {\n return;\n }\n\n const { systemPrompt, conversationMessages } = splitSystemMessage(\n state.messages\n );\n const cutoffIndex = await determineCutoffIndex(\n conversationMessages,\n validatedKeep,\n tokenCounter,\n model\n );\n\n if (cutoffIndex <= 0) {\n return;\n }\n\n const { messagesToSummarize, preservedMessages } = partitionMessages(\n systemPrompt,\n conversationMessages,\n cutoffIndex\n );\n\n const summary = await createSummary(\n messagesToSummarize,\n model,\n summaryPrompt,\n tokenCounter,\n trimTokensToSummarize,\n runtime\n );\n\n // Reuse a replaced message's id so the summary projects as an update, not a new message.\n const summaryMessage = new HumanMessage({\n content: `${summaryPrefix}\\n\\n${summary}`,\n id: conversationMessages[0].id,\n additional_kwargs: { lc_source: \"summarization\" },\n });\n\n return {\n messages: [\n new RemoveMessage({ id: REMOVE_ALL_MESSAGES }),\n summaryMessage,\n ...preservedMessages,\n ],\n };\n },\n });\n}\n\n/**\n * Ensure all messages have unique IDs\n */\nfunction ensureMessageIds(messages: BaseMessage[]): void {\n for (const msg of messages) {\n if (!msg.id) {\n msg.id = uuid();\n }\n }\n}\n\n/**\n * Separate system message from conversation messages\n */\nfunction splitSystemMessage(messages: BaseMessage[]): {\n systemPrompt?: SystemMessage;\n conversationMessages: BaseMessage[];\n} {\n if (messages.length > 0 && SystemMessage.isInstance(messages[0])) {\n return {\n systemPrompt: messages[0] as SystemMessage,\n conversationMessages: messages.slice(1),\n };\n }\n return {\n conversationMessages: messages,\n };\n}\n\n/**\n * Partition messages into those to summarize and those to preserve\n */\nfunction partitionMessages(\n systemPrompt: SystemMessage | undefined,\n conversationMessages: BaseMessage[],\n cutoffIndex: number\n): { messagesToSummarize: BaseMessage[]; preservedMessages: BaseMessage[] } {\n const messagesToSummarize = conversationMessages.slice(0, cutoffIndex);\n const preservedMessages = conversationMessages.slice(cutoffIndex);\n\n // Include system message in messages to summarize to capture previous summaries\n if (systemPrompt) {\n messagesToSummarize.unshift(systemPrompt);\n }\n\n return { messagesToSummarize, preservedMessages };\n}\n\n/**\n * Determine whether summarization should run for the current token usage\n *\n * @param messages - Current messages in the conversation\n * @param totalTokens - Total token count for all messages\n * @param triggerConditions - Array of trigger conditions. Returns true if ANY condition is satisfied (OR logic).\n * Within each condition, ALL specified properties must be satisfied (AND logic).\n * @param model - The language model being used\n * @returns true if summarization should be triggered\n */\nasync function shouldSummarize(\n messages: BaseMessage[],\n totalTokens: number,\n triggerConditions: ContextSize[],\n model: BaseLanguageModel\n): Promise<boolean> {\n if (triggerConditions.length === 0) {\n return false;\n }\n\n /**\n * Check each condition (OR logic between conditions)\n */\n for (const trigger of triggerConditions) {\n /**\n * Within a single condition, all specified properties must be satisfied (AND logic)\n */\n let conditionMet = true;\n let hasAnyProperty = false;\n\n if (trigger.messages !== undefined) {\n hasAnyProperty = true;\n if (messages.length < trigger.messages) {\n conditionMet = false;\n }\n }\n\n if (trigger.tokens !== undefined) {\n hasAnyProperty = true;\n if (totalTokens < trigger.tokens) {\n conditionMet = false;\n }\n }\n\n if (trigger.fraction !== undefined) {\n hasAnyProperty = true;\n const maxInputTokens = getProfileLimits(model);\n if (typeof maxInputTokens === \"number\") {\n const threshold = Math.floor(maxInputTokens * trigger.fraction);\n if (totalTokens < threshold) {\n conditionMet = false;\n }\n } else {\n /**\n * If fraction is specified but we can't get model limits, skip this condition\n */\n conditionMet = false;\n }\n }\n\n /**\n * If condition has at least one property and all properties are satisfied, trigger summarization\n */\n if (hasAnyProperty && conditionMet) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Determine cutoff index respecting retention configuration\n */\nasync function determineCutoffIndex(\n messages: BaseMessage[],\n keep: ContextSize,\n tokenCounter: TokenCounter,\n model: BaseLanguageModel\n): Promise<number> {\n if (\"tokens\" in keep || \"fraction\" in keep) {\n const tokenBasedCutoff = await findTokenBasedCutoff(\n messages,\n keep,\n tokenCounter,\n model\n );\n if (typeof tokenBasedCutoff === \"number\") {\n return tokenBasedCutoff;\n }\n /**\n * Fallback to message count if token-based fails\n */\n return findSafeCutoff(messages, DEFAULT_MESSAGES_TO_KEEP);\n }\n /**\n * find cutoff index based on message count\n */\n return findSafeCutoff(messages, keep.messages ?? DEFAULT_MESSAGES_TO_KEEP);\n}\n\n/**\n * Find cutoff index based on target token retention\n */\nasync function findTokenBasedCutoff(\n messages: BaseMessage[],\n keep: ContextSize,\n tokenCounter: TokenCounter,\n model: BaseLanguageModel\n): Promise<number | undefined> {\n if (messages.length === 0) {\n return 0;\n }\n\n let targetTokenCount: number;\n\n if (\"fraction\" in keep && keep.fraction !== undefined) {\n const maxInputTokens = getProfileLimits(model);\n if (typeof maxInputTokens !== \"number\") {\n return;\n }\n targetTokenCount = Math.floor(maxInputTokens * keep.fraction);\n } else if (\"tokens\" in keep && keep.tokens !== undefined) {\n targetTokenCount = Math.floor(keep.tokens);\n } else {\n return;\n }\n\n if (targetTokenCount <= 0) {\n targetTokenCount = 1;\n }\n\n const totalTokens = await tokenCounter(messages);\n if (totalTokens <= targetTokenCount) {\n return 0;\n }\n\n /**\n * Use binary search to identify the earliest message index that keeps the\n * suffix within the token budget.\n */\n let left = 0;\n let right = messages.length;\n let cutoffCandidate = messages.length;\n const maxIterations = Math.floor(Math.log2(messages.length)) + 1;\n\n for (let i = 0; i < maxIterations; i++) {\n if (left >= right) {\n break;\n }\n\n const mid = Math.floor((left + right) / 2);\n const suffixTokens = await tokenCounter(messages.slice(mid));\n if (suffixTokens <= targetTokenCount) {\n cutoffCandidate = mid;\n right = mid;\n } else {\n left = mid + 1;\n }\n }\n\n if (cutoffCandidate === messages.length) {\n cutoffCandidate = left;\n }\n\n if (cutoffCandidate >= messages.length) {\n if (messages.length === 1) {\n return 0;\n }\n cutoffCandidate = messages.length - 1;\n }\n\n /**\n * Find safe cutoff point that preserves AI/Tool pairs.\n * If cutoff lands on ToolMessage, move backward to include the AIMessage.\n */\n const safeCutoff = findSafeCutoffPoint(messages, cutoffCandidate);\n\n /**\n * If findSafeCutoffPoint moved forward (fallback case), verify it's safe.\n * If it moved backward, we already have a safe point.\n */\n if (safeCutoff <= cutoffCandidate) {\n return safeCutoff;\n }\n\n /**\n * Fallback: iterate backward to find a safe cutoff\n */\n for (let i = cutoffCandidate; i >= 0; i--) {\n if (isSafeCutoffPoint(messages, i)) {\n return i;\n }\n }\n\n return 0;\n}\n\n/**\n * Find safe cutoff point that preserves AI/Tool message pairs\n */\nfunction findSafeCutoff(\n messages: BaseMessage[],\n messagesToKeep: number\n): number {\n if (messages.length <= messagesToKeep) {\n return 0;\n }\n\n const targetCutoff = messages.length - messagesToKeep;\n\n /**\n * First, try to find a safe cutoff point using findSafeCutoffPoint.\n * This handles the case where cutoff lands on a ToolMessage by moving\n * backward to include the corresponding AIMessage.\n */\n const safeCutoff = findSafeCutoffPoint(messages, targetCutoff);\n\n /**\n * If findSafeCutoffPoint moved backward (found matching AIMessage), use it.\n */\n if (safeCutoff <= targetCutoff) {\n return safeCutoff;\n }\n\n /**\n * Fallback: iterate backward to find a safe cutoff\n */\n for (let i = targetCutoff; i >= 0; i--) {\n if (isSafeCutoffPoint(messages, i)) {\n return i;\n }\n }\n\n return 0;\n}\n\n/**\n * Check if cutting at index would separate AI/Tool message pairs\n */\nfunction isSafeCutoffPoint(\n messages: BaseMessage[],\n cutoffIndex: number\n): boolean {\n if (cutoffIndex >= messages.length) {\n return true;\n }\n\n /**\n * Prevent preserved messages from starting with AI message containing tool calls\n */\n if (\n cutoffIndex < messages.length &&\n AIMessage.isInstance(messages[cutoffIndex]) &&\n hasToolCalls(messages[cutoffIndex])\n ) {\n return false;\n }\n\n const searchStart = Math.max(0, cutoffIndex - SEARCH_RANGE_FOR_TOOL_PAIRS);\n const searchEnd = Math.min(\n messages.length,\n cutoffIndex + SEARCH_RANGE_FOR_TOOL_PAIRS\n );\n\n for (let i = searchStart; i < searchEnd; i++) {\n if (!hasToolCalls(messages[i])) {\n continue;\n }\n\n const toolCallIds = extractToolCallIds(messages[i] as AIMessage);\n if (cutoffSeparatesToolPair(messages, i, cutoffIndex, toolCallIds)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Extract tool call IDs from an AI message\n */\nfunction extractToolCallIds(aiMessage: AIMessage): Set<string> {\n const toolCallIds = new Set<string>();\n if (aiMessage.tool_calls) {\n for (const toolCall of aiMessage.tool_calls) {\n const id =\n typeof toolCall === \"object\" && \"id\" in toolCall ? toolCall.id : null;\n if (id) {\n toolCallIds.add(id);\n }\n }\n }\n return toolCallIds;\n}\n\n/**\n * Find a safe cutoff point that doesn't split AI/Tool message pairs.\n *\n * If the message at `cutoffIndex` is a `ToolMessage`, search backward for the\n * `AIMessage` containing the corresponding `tool_calls` and adjust the cutoff to\n * include it. This ensures tool call requests and responses stay together.\n *\n * Falls back to advancing forward past `ToolMessage` objects only if no matching\n * `AIMessage` is found (edge case).\n */\nfunction findSafeCutoffPoint(\n messages: BaseMessage[],\n cutoffIndex: number\n): number {\n if (\n cutoffIndex >= messages.length ||\n !ToolMessage.isInstance(messages[cutoffIndex])\n ) {\n return cutoffIndex;\n }\n\n // Collect tool_call_ids from consecutive ToolMessages at/after cutoff\n const toolCallIds = new Set<string>();\n let idx = cutoffIndex;\n while (idx < messages.length && ToolMessage.isInstance(messages[idx])) {\n const toolMsg = messages[idx] as ToolMessage;\n if (toolMsg.tool_call_id) {\n toolCallIds.add(toolMsg.tool_call_id);\n }\n idx++;\n }\n\n // Search backward for AIMessage with matching tool_calls\n for (let i = cutoffIndex - 1; i >= 0; i--) {\n const msg = messages[i];\n if (AIMessage.isInstance(msg) && hasToolCalls(msg)) {\n const aiToolCallIds = extractToolCallIds(msg as AIMessage);\n // Check if there's any overlap between the tool_call_ids\n for (const id of toolCallIds) {\n if (aiToolCallIds.has(id)) {\n // Found the AIMessage - move cutoff to include it\n return i;\n }\n }\n }\n }\n\n // Fallback: no matching AIMessage found, advance past ToolMessages to avoid\n // orphaned tool responses\n return idx;\n}\n\n/**\n * Check if cutoff separates an AI message from its corresponding tool messages\n */\nfunction cutoffSeparatesToolPair(\n messages: BaseMessage[],\n aiMessageIndex: number,\n cutoffIndex: number,\n toolCallIds: Set<string>\n): boolean {\n for (let j = aiMessageIndex + 1; j < messages.length; j++) {\n const message = messages[j];\n if (\n ToolMessage.isInstance(message) &&\n toolCallIds.has(message.tool_call_id)\n ) {\n const aiBeforeCutoff = aiMessageIndex < cutoffIndex;\n const toolBeforeCutoff = j < cutoffIndex;\n if (aiBeforeCutoff !== toolBeforeCutoff) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Generate summary for the given messages.\n *\n * @param messagesToSummarize - Messages to summarize.\n * @param model - The language model to use for summarization.\n * @param summaryPrompt - The prompt template for summarization.\n * @param tokenCounter - Function to count tokens.\n * @param trimTokensToSummarize - Optional token limit for trimming messages.\n * @param runtime - The runtime environment, used to inherit config so that\n * LangGraph's handlers can properly track and tag the summarization model call.\n */\nasync function createSummary(\n messagesToSummarize: BaseMessage[],\n model: BaseLanguageModel,\n summaryPrompt: string,\n tokenCounter: TokenCounter,\n trimTokensToSummarize: number | undefined,\n runtime: Runtime<unknown>\n): Promise<string> {\n if (!messagesToSummarize.length) {\n return \"No previous conversation history.\";\n }\n\n const trimmedMessages = await trimMessagesForSummary(\n messagesToSummarize,\n tokenCounter,\n trimTokensToSummarize\n );\n\n if (!trimmedMessages.length) {\n return \"Previous conversation was too long to summarize.\";\n }\n\n /**\n * Format messages using getBufferString to avoid token inflation from metadata\n * when str() / JSON.stringify is called on message objects.\n * This produces compact output like:\n * ```\n * Human: What's the weather?\n * AI: Let me check...[tool_calls]\n * Tool: 72°F and sunny\n * ```\n */\n const formattedMessages = getBufferString(trimmedMessages);\n\n try {\n const formattedPrompt = summaryPrompt.replace(\n \"{messages}\",\n formattedMessages\n );\n /**\n * Merge parent runnable config with summarization metadata so LangGraph's\n * stream handlers (and other callback-based consumers) can properly track\n * and tag the summarization model call.\n */\n const baseConfig: RunnableConfig = pickRunnableConfigKeys(runtime) ?? {};\n const config = mergeConfigs(baseConfig, {\n metadata: { lc_source: \"summarization\" },\n tags: [INTERNAL_CALL_TAG],\n });\n const response = await model.invoke(formattedPrompt, config);\n const content = response.content;\n /**\n * Handle both string content and MessageContent array\n */\n if (typeof content === \"string\") {\n return content.trim();\n } else if (Array.isArray(content)) {\n /**\n * Extract text from MessageContent array\n */\n const textContent = content\n .map((item) => {\n if (typeof item === \"string\") return item;\n if (typeof item === \"object\" && item !== null && \"text\" in item) {\n return (item as { text: string }).text;\n }\n return \"\";\n })\n .join(\"\");\n return textContent.trim();\n }\n return \"Error generating summary: Invalid response format\";\n } catch (e) {\n return `Error generating summary: ${e}`;\n }\n}\n\n/**\n * Trim messages to fit within summary generation limits\n */\nasync function trimMessagesForSummary(\n messages: BaseMessage[],\n tokenCounter: TokenCounter,\n trimTokensToSummarize: number | undefined\n): Promise<BaseMessage[]> {\n if (trimTokensToSummarize === undefined) {\n return messages;\n }\n\n try {\n return await trimMessages(messages, {\n maxTokens: trimTokensToSummarize,\n tokenCounter: async (msgs) => tokenCounter(msgs),\n strategy: \"last\",\n allowPartial: true,\n includeSystem: true,\n });\n } catch {\n /**\n * Fallback to last N messages if trimming fails\n */\n return messages.slice(-DEFAULT_FALLBACK_MESSAGE_COUNT);\n }\n}\n"],"mappings":"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"}
|
|
@@ -259,8 +259,9 @@ model: z$1.custom().optional() }),
|
|
|
259
259
|
const cutoffIndex = await determineCutoffIndex(conversationMessages, validatedKeep, tokenCounter, model);
|
|
260
260
|
if (cutoffIndex <= 0) return;
|
|
261
261
|
const { messagesToSummarize, preservedMessages } = partitionMessages(systemPrompt, conversationMessages, cutoffIndex);
|
|
262
|
+
const summary = await createSummary(messagesToSummarize, model, summaryPrompt, tokenCounter, trimTokensToSummarize, runtime);
|
|
262
263
|
const summaryMessage = new HumanMessage({
|
|
263
|
-
content: `${summaryPrefix}\n\n${
|
|
264
|
+
content: `${summaryPrefix}\n\n${summary}`,
|
|
264
265
|
id: conversationMessages[0].id,
|
|
265
266
|
additional_kwargs: { lc_source: "summarization" }
|
|
266
267
|
});
|
|
@@ -534,7 +535,13 @@ async function createSummary(messagesToSummarize, model, summaryPrompt, tokenCou
|
|
|
534
535
|
const formattedMessages = getBufferString(trimmedMessages);
|
|
535
536
|
try {
|
|
536
537
|
const formattedPrompt = summaryPrompt.replace("{messages}", formattedMessages);
|
|
537
|
-
|
|
538
|
+
/**
|
|
539
|
+
* Merge parent runnable config with summarization metadata so LangGraph's
|
|
540
|
+
* stream handlers (and other callback-based consumers) can properly track
|
|
541
|
+
* and tag the summarization model call.
|
|
542
|
+
*/
|
|
543
|
+
const baseConfig = pickRunnableConfigKeys(runtime) ?? {};
|
|
544
|
+
const config = mergeConfigs(baseConfig, {
|
|
538
545
|
metadata: { lc_source: "summarization" },
|
|
539
546
|
tags: [INTERNAL_CALL_TAG]
|
|
540
547
|
});
|