UNPKG

langchain

Version:
1 lines 18.8 kB
{"version":3,"file":"contextEditing.cjs","names":["config: ClearToolUsesEditConfig","params: {\n tokens: number;\n messages: BaseMessage[];\n countTokens: TokenCounter;\n }","candidates: Array<{ idx: number; msg: ToolMessage }>","ToolMessage","#findAIMessageForToolCall","#buildClearedToolInputMessage","previousMessages: BaseMessage[]","toolCallId: string","AIMessage","message: AIMessage","config: ContextEditingMiddlewareConfig","createMiddleware","SystemMessage","countTokens: TokenCounter","countTokensApproximately","messages: BaseMessage[]"],"sources":["../../../src/agents/middleware/contextEditing.ts"],"sourcesContent":["/**\n * Context editing middleware.\n *\n * This middleware mirrors Anthropic's context editing capabilities by clearing\n * older tool results once the conversation grows beyond a configurable token\n * threshold. The implementation is intentionally model-agnostic so it can be used\n * with any LangChain chat model.\n */\n\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport type { LanguageModelLike } from \"@langchain/core/language_models/base\";\nimport {\n AIMessage,\n ToolMessage,\n SystemMessage,\n} from \"@langchain/core/messages\";\n\nimport { countTokensApproximately } from \"./utils.js\";\nimport { createMiddleware } from \"../middleware.js\";\n\nconst DEFAULT_TOOL_PLACEHOLDER = \"[cleared]\";\n\n/**\n * Function type for counting tokens in a sequence of messages.\n */\nexport type TokenCounter = (\n messages: BaseMessage[]\n) => number | Promise<number>;\n\n/**\n * Protocol describing a context editing strategy.\n *\n * Implement this interface to create custom strategies for managing\n * conversation context size. The `apply` method should modify the\n * messages array in-place and return the updated token count.\n *\n * @example\n * ```ts\n * import { SystemMessage } from \"langchain\";\n *\n * class RemoveOldSystemMessages implements ContextEdit {\n * async apply({ tokens, messages, countTokens }) {\n * // Remove old system messages if over limit\n * if (tokens > 50000) {\n * messages = messages.filter(SystemMessage.isInstance);\n * return await countTokens(messages);\n * }\n * return tokens;\n * }\n * }\n * ```\n */\nexport interface ContextEdit {\n /**\n * Apply an edit to the message list, returning the new token count.\n *\n * This method should:\n * 1. Check if editing is needed based on `tokens` parameter\n * 2. Modify the `messages` array in-place (if needed)\n * 3. Return the new token count after modifications\n *\n * @param params - Parameters for the editing operation\n * @returns The updated token count after applying edits\n */\n apply(params: {\n /**\n * Current token count of all messages\n */\n tokens: number;\n /**\n * Array of messages to potentially edit (modify in-place)\n */\n messages: BaseMessage[];\n /**\n * Function to count tokens in a message array\n */\n countTokens: TokenCounter;\n }): number | Promise<number>;\n}\n\n/**\n * Configuration for clearing tool outputs when token limits are exceeded.\n */\nexport interface ClearToolUsesEditConfig {\n /**\n * Token count that triggers the edit.\n * @default 100000\n */\n triggerTokens?: number;\n\n /**\n * Minimum number of tokens to reclaim when the edit runs.\n * @default 0\n */\n clearAtLeast?: number;\n\n /**\n * Number of most recent tool results that must be preserved.\n * @default 3\n */\n keep?: number;\n\n /**\n * Whether to clear the originating tool call parameters on the AI message.\n * @default false\n */\n clearToolInputs?: boolean;\n\n /**\n * List of tool names to exclude from clearing.\n * @default []\n */\n excludeTools?: string[];\n\n /**\n * Placeholder text inserted for cleared tool outputs.\n * @default \"[cleared]\"\n */\n placeholder?: string;\n}\n\n/**\n * Strategy for clearing tool outputs when token limits are exceeded.\n *\n * This strategy mirrors Anthropic's `clear_tool_uses_20250919` behavior by\n * replacing older tool results with a placeholder text when the conversation\n * grows too large. It preserves the most recent tool results and can exclude\n * specific tools from being cleared.\n *\n * @example\n * ```ts\n * import { ClearToolUsesEdit } from \"langchain\";\n *\n * const edit = new ClearToolUsesEdit({\n * triggerTokens: 100000, // Start clearing at 100K tokens\n * clearAtLeast: 0, // Clear as much as needed\n * keep: 3, // Always keep 3 most recent results\n * excludeTools: [\"important\"], // Never clear \"important\" tool\n * clearToolInputs: false, // Keep tool call arguments\n * placeholder: \"[cleared]\", // Replacement text\n * });\n * ```\n */\nexport class ClearToolUsesEdit implements ContextEdit {\n triggerTokens: number;\n clearAtLeast: number;\n keep: number;\n clearToolInputs: boolean;\n excludeTools: Set<string>;\n placeholder: string;\n\n constructor(config: ClearToolUsesEditConfig = {}) {\n this.triggerTokens = config.triggerTokens ?? 100000;\n this.clearAtLeast = config.clearAtLeast ?? 0;\n this.keep = config.keep ?? 3;\n this.clearToolInputs = config.clearToolInputs ?? false;\n this.excludeTools = new Set(config.excludeTools ?? []);\n this.placeholder = config.placeholder ?? DEFAULT_TOOL_PLACEHOLDER;\n }\n\n async apply(params: {\n tokens: number;\n messages: BaseMessage[];\n countTokens: TokenCounter;\n }): Promise<number> {\n const { tokens, messages, countTokens } = params;\n\n if (tokens <= this.triggerTokens) {\n return tokens;\n }\n\n /**\n * Find all tool message candidates with their actual indices in the messages array\n */\n const candidates: Array<{ idx: number; msg: ToolMessage }> = [];\n for (let i = 0; i < messages.length; i++) {\n const msg = messages[i];\n if (ToolMessage.isInstance(msg)) {\n candidates.push({ idx: i, msg });\n }\n }\n\n /**\n * Keep the most recent tool messages\n */\n const candidatesToClear =\n this.keep >= candidates.length\n ? []\n : this.keep > 0\n ? candidates.slice(0, -this.keep)\n : candidates;\n\n let clearedTokens = 0;\n for (const { idx, msg: toolMessage } of candidatesToClear) {\n /**\n * Stop if we've cleared enough tokens\n */\n if (this.clearAtLeast > 0 && clearedTokens >= this.clearAtLeast) {\n break;\n }\n\n /**\n * Skip if already cleared\n */\n const contextEditing = toolMessage.response_metadata?.context_editing as\n | { cleared?: boolean }\n | undefined;\n if (contextEditing?.cleared) {\n continue;\n }\n\n /**\n * Find the corresponding AI message\n */\n const aiMessage = this.#findAIMessageForToolCall(\n messages.slice(0, idx),\n toolMessage.tool_call_id\n );\n\n if (!aiMessage) {\n continue;\n }\n\n /**\n * Find the corresponding tool call\n */\n const toolCall = aiMessage.tool_calls?.find(\n (call) => call.id === toolMessage.tool_call_id\n );\n\n if (!toolCall) {\n continue;\n }\n\n /**\n * Skip if tool is excluded\n */\n const toolName = toolMessage.name || toolCall.name;\n if (this.excludeTools.has(toolName)) {\n continue;\n }\n\n /**\n * Clear the tool message\n */\n messages[idx] = new ToolMessage({\n tool_call_id: toolMessage.tool_call_id,\n content: this.placeholder,\n name: toolMessage.name,\n artifact: undefined,\n response_metadata: {\n ...toolMessage.response_metadata,\n context_editing: {\n cleared: true,\n strategy: \"clear_tool_uses\",\n },\n },\n });\n\n /**\n * Optionally clear the tool inputs\n */\n if (this.clearToolInputs) {\n const aiMsgIdx = messages.indexOf(aiMessage);\n if (aiMsgIdx >= 0) {\n messages[aiMsgIdx] = this.#buildClearedToolInputMessage(\n aiMessage,\n toolMessage.tool_call_id\n );\n }\n }\n\n /**\n * Recalculate tokens\n */\n const newTokenCount = await countTokens(messages);\n clearedTokens = Math.max(0, tokens - newTokenCount);\n }\n\n return tokens - clearedTokens;\n }\n\n #findAIMessageForToolCall(\n previousMessages: BaseMessage[],\n toolCallId: string\n ): AIMessage | null {\n // Search backwards through previous messages\n for (let i = previousMessages.length - 1; i >= 0; i--) {\n const msg = previousMessages[i];\n if (AIMessage.isInstance(msg)) {\n const hasToolCall = msg.tool_calls?.some(\n (call) => call.id === toolCallId\n );\n if (hasToolCall) {\n return msg;\n }\n }\n }\n return null;\n }\n\n #buildClearedToolInputMessage(\n message: AIMessage,\n toolCallId: string\n ): AIMessage {\n const updatedToolCalls = message.tool_calls?.map((toolCall) => {\n if (toolCall.id === toolCallId) {\n return { ...toolCall, args: {} };\n }\n return toolCall;\n });\n\n const metadata = { ...message.response_metadata };\n const contextEntry = {\n ...(metadata.context_editing as Record<string, unknown>),\n };\n\n const clearedIds = new Set<string>(\n contextEntry.cleared_tool_inputs as string[] | undefined\n );\n clearedIds.add(toolCallId);\n contextEntry.cleared_tool_inputs = Array.from(clearedIds).sort();\n metadata.context_editing = contextEntry;\n\n return new AIMessage({\n content: message.content,\n tool_calls: updatedToolCalls,\n response_metadata: metadata,\n id: message.id,\n name: message.name,\n additional_kwargs: message.additional_kwargs,\n });\n }\n}\n\n/**\n * Configuration for the Context Editing Middleware.\n */\nexport interface ContextEditingMiddlewareConfig {\n /**\n * Sequence of edit strategies to apply. Defaults to a single\n * ClearToolUsesEdit mirroring Anthropic defaults.\n */\n edits?: ContextEdit[];\n\n /**\n * Whether to use approximate token counting (faster, less accurate)\n * or exact counting implemented by the chat model (potentially slower, more accurate).\n * Currently only OpenAI models support exact counting.\n * @default \"approx\"\n */\n tokenCountMethod?: \"approx\" | \"model\";\n}\n\n/**\n * Middleware that automatically prunes tool results to manage context size.\n *\n * This middleware applies a sequence of edits when the total input token count\n * exceeds configured thresholds. By default, it uses the `ClearToolUsesEdit` strategy\n * which mirrors Anthropic's `clear_tool_uses_20250919` behaviour by clearing older\n * tool results once the conversation exceeds 100,000 tokens.\n *\n * ## Basic Usage\n *\n * Use the middleware with default settings to automatically manage context:\n *\n * @example Basic usage with defaults\n * ```ts\n * import { contextEditingMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"anthropic:claude-3-5-sonnet\",\n * tools: [searchTool, calculatorTool],\n * middleware: [\n * contextEditingMiddleware(),\n * ],\n * });\n * ```\n *\n * The default configuration:\n * - Triggers when context exceeds **100,000 tokens**\n * - Keeps the **3 most recent** tool results\n * - Uses **approximate token counting** (fast)\n * - Does not clear tool call arguments\n *\n * ## Custom Configuration\n *\n * Customize the clearing behavior with `ClearToolUsesEdit`:\n *\n * @example Custom ClearToolUsesEdit configuration\n * ```ts\n * import { contextEditingMiddleware, ClearToolUsesEdit } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"anthropic:claude-3-5-sonnet\",\n * tools: [searchTool, calculatorTool],\n * middleware: [\n * contextEditingMiddleware({\n * edits: [\n * new ClearToolUsesEdit({\n * triggerTokens: 50000, // Clear when exceeding 50K tokens\n * clearAtLeast: 1000, // Reclaim at least 1K tokens\n * keep: 5, // Keep 5 most recent tool results\n * excludeTools: [\"search\"], // Never clear search results\n * clearToolInputs: true, // Also clear tool call arguments\n * }),\n * ],\n * tokenCountMethod: \"approx\", // Use approximate counting (or \"model\")\n * }),\n * ],\n * });\n * ```\n *\n * ## Custom Editing Strategies\n *\n * Implement your own context editing strategy by creating a class that\n * implements the `ContextEdit` interface:\n *\n * @example Custom editing strategy\n * ```ts\n * import { contextEditingMiddleware, type ContextEdit, type TokenCounter } from \"langchain\";\n * import type { BaseMessage } from \"@langchain/core/messages\";\n *\n * class CustomEdit implements ContextEdit {\n * async apply(params: {\n * tokens: number;\n * messages: BaseMessage[];\n * countTokens: TokenCounter;\n * }): Promise<number> {\n * // Implement your custom editing logic here\n * // and apply it to the messages array, then\n * // return the new token count after edits\n * return countTokens(messages);\n * }\n * }\n * ```\n *\n * @param config - Configuration options for the middleware\n * @returns A middleware instance that can be used with `createAgent`\n */\nexport function contextEditingMiddleware(\n config: ContextEditingMiddlewareConfig = {}\n) {\n const edits = config.edits ?? [new ClearToolUsesEdit()];\n const tokenCountMethod = config.tokenCountMethod ?? \"approx\";\n\n return createMiddleware({\n name: \"ContextEditingMiddleware\",\n wrapModelCall: async (request, handler) => {\n if (!request.messages || request.messages.length === 0) {\n return handler(request);\n }\n\n /**\n * Use model's token counting method\n */\n const systemMsg = request.systemPrompt\n ? [new SystemMessage(request.systemPrompt)]\n : [];\n\n const countTokens: TokenCounter =\n tokenCountMethod === \"approx\"\n ? countTokensApproximately\n : async (messages: BaseMessage[]): Promise<number> => {\n const allMessages = [...systemMsg, ...messages];\n\n /**\n * Check if model has getNumTokensFromMessages method\n * currently only OpenAI models have this method\n */\n if (\"getNumTokensFromMessages\" in request.model) {\n return (\n request.model as LanguageModelLike & {\n getNumTokensFromMessages: (\n messages: BaseMessage[]\n ) => Promise<{\n totalCount: number;\n countPerMessage: number[];\n }>;\n }\n )\n .getNumTokensFromMessages(allMessages)\n .then(({ totalCount }) => totalCount);\n }\n\n throw new Error(\n `Model \"${request.model.getName()}\" does not support token counting`\n );\n };\n\n let tokens = await countTokens(request.messages);\n\n /**\n * Apply each edit in sequence\n */\n for (const edit of edits) {\n tokens = await edit.apply({\n tokens,\n messages: request.messages,\n countTokens,\n });\n }\n\n return handler(request);\n },\n });\n}\n"],"mappings":"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"}