{"version":3,"file":"contextEditing.cjs","names":["#triggerConditions","contextSizeSchema","keepSchema","ToolMessage","#findAIMessageForToolCall","#shouldEdit","#determineKeepCount","#buildClearedToolInputMessage","getProfileLimits","AIMessage","createMiddleware","SystemMessage","countTokensApproximately"],"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 { BaseLanguageModel } 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\";\nimport {\n  getProfileLimits,\n  contextSizeSchema,\n  keepSchema,\n  type ContextSize,\n  type KeepSize,\n  type TokenCounter,\n} from \"./summarization.js\";\n\nconst DEFAULT_TOOL_PLACEHOLDER = \"[cleared]\";\nconst DEFAULT_TRIGGER_TOKENS = 100_000;\nconst DEFAULT_KEEP = 3;\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 { HumanMessage, type ContextEdit, type BaseMessage  } from \"langchain\";\n *\n * class RemoveOldHumanMessages implements ContextEdit {\n *   constructor(private keepRecent: number = 10) {}\n *\n *   async apply({ messages, countTokens }) {\n *     // Check current token count\n *     const tokens = await countTokens(messages);\n *\n *     // Remove old human messages if over limit, keeping the most recent ones\n *     if (tokens > 50000) {\n *       const humanMessages: number[] = [];\n *\n *       // Find all human message indices\n *       for (let i = 0; i < messages.length; i++) {\n *         if (HumanMessage.isInstance(messages[i])) {\n *           humanMessages.push(i);\n *         }\n *       }\n *\n *       // Remove old human messages (keep only the most recent N)\n *       const toRemove = humanMessages.slice(0, -this.keepRecent);\n *       for (let i = toRemove.length - 1; i >= 0; i--) {\n *         messages.splice(toRemove[i]!, 1);\n *       }\n *     }\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     * 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    /**\n     * Optional model instance for model profile information\n     */\n    model?: BaseLanguageModel;\n  }): void | Promise<void>;\n}\n\n/**\n * Configuration for clearing tool outputs when token limits are exceeded.\n */\nexport interface ClearToolUsesEditConfig {\n  /**\n   * Trigger conditions for context editing.\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 >= 100000 AND messages >= 50\n   * trigger: { tokens: 100000, messages: 50 }\n   *\n   * // Multiple conditions: trigger if (tokens >= 100000 AND messages >= 50) OR (tokens >= 50000 AND messages >= 100)\n   * trigger: [\n   *   { tokens: 100000, messages: 50 },\n   *   { tokens: 50000, messages: 100 }\n   * ]\n   *\n   * // Fractional trigger: trigger at 80% of model's max input tokens\n   * trigger: { fraction: 0.8 }\n   * ```\n   */\n  trigger?: ContextSize | ContextSize[];\n\n  /**\n   * Context retention policy applied after editing.\n   * Specify how many tool results to preserve using messages, tokens, or fraction.\n   *\n   * @example\n   * ```ts\n   * // Keep 3 most recent tool results\n   * keep: { messages: 3 }\n   *\n   * // Keep tool results that fit within 1000 tokens\n   * keep: { tokens: 1000 }\n   *\n   * // Keep tool results that fit within 30% of model's max input tokens\n   * keep: { fraction: 0.3 }\n   * ```\n   */\n  keep?: KeepSize;\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   * @deprecated Use `trigger: { tokens: value }` instead.\n   */\n  triggerTokens?: number;\n\n  /**\n   * @deprecated Use `keep: { messages: value }` instead.\n   */\n  keepMessages?: number;\n\n  /**\n   * @deprecated This property is deprecated and will be removed in a future version.\n   * Use `keep: { tokens: value }` or `keep: { messages: value }` instead to control retention.\n   */\n  clearAtLeast?: number;\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 *   trigger: { tokens: 100000 },  // Start clearing at 100K tokens\n *   keep: { messages: 3 },        // Keep 3 most recent tool results\n *   excludeTools: [\"important\"],   // Never clear \"important\" tool\n *   clearToolInputs: false,        // Keep tool call arguments\n *   placeholder: \"[cleared]\",      // Replacement text\n * });\n *\n * // Multiple trigger conditions\n * const edit2 = new ClearToolUsesEdit({\n *   trigger: [\n *     { tokens: 100000, messages: 50 },\n *     { tokens: 50000, messages: 100 }\n *   ],\n *   keep: { messages: 3 },\n * });\n *\n * // Fractional trigger with model profile\n * const edit3 = new ClearToolUsesEdit({\n *   trigger: { fraction: 0.8 },  // Trigger at 80% of model's max tokens\n *   keep: { fraction: 0.3 },     // Keep 30% of model's max tokens\n * });\n * ```\n */\nexport class ClearToolUsesEdit implements ContextEdit {\n  #triggerConditions: ContextSize[];\n\n  trigger: ContextSize | ContextSize[];\n  keep: KeepSize;\n  clearToolInputs: boolean;\n  excludeTools: Set<string>;\n  placeholder: string;\n  model: BaseLanguageModel;\n  clearAtLeast: number;\n\n  constructor(config: ClearToolUsesEditConfig = {}) {\n    // Handle deprecated parameters\n    let trigger: ContextSize | ContextSize[] | undefined = config.trigger;\n    if (config.triggerTokens !== undefined) {\n      console.warn(\n        \"triggerTokens is deprecated. Use `trigger: { tokens: value }` instead.\"\n      );\n      if (trigger === undefined) {\n        trigger = { tokens: config.triggerTokens };\n      }\n    }\n\n    let keep: KeepSize | undefined = config.keep;\n    if (config.keepMessages !== undefined) {\n      console.warn(\n        \"keepMessages is deprecated. Use `keep: { messages: value }` instead.\"\n      );\n      if (keep === undefined) {\n        keep = { messages: config.keepMessages };\n      }\n    }\n\n    // Set defaults\n    if (trigger === undefined) {\n      trigger = { tokens: DEFAULT_TRIGGER_TOKENS };\n    }\n    if (keep === undefined) {\n      keep = { messages: DEFAULT_KEEP };\n    }\n\n    // Validate trigger conditions\n    if (Array.isArray(trigger)) {\n      this.#triggerConditions = trigger.map((t) => contextSizeSchema.parse(t));\n      this.trigger = this.#triggerConditions;\n    } else {\n      const validated = contextSizeSchema.parse(trigger);\n      this.#triggerConditions = [validated];\n      this.trigger = validated;\n    }\n\n    // Validate keep\n    const validatedKeep = keepSchema.parse(keep);\n    this.keep = validatedKeep;\n\n    // Handle deprecated clearAtLeast\n    if (config.clearAtLeast !== undefined) {\n      console.warn(\n        \"clearAtLeast is deprecated and will be removed in a future version. \" +\n          \"It conflicts with the `keep` property. Use `keep: { tokens: value }` or \" +\n          \"`keep: { messages: value }` instead to control retention.\"\n      );\n    }\n    this.clearAtLeast = config.clearAtLeast ?? 0;\n\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    messages: BaseMessage[];\n    model: BaseLanguageModel;\n    countTokens: TokenCounter;\n  }): Promise<void> {\n    const { messages, model, countTokens } = params;\n    const tokens = await countTokens(messages);\n\n    /**\n     * Always remove orphaned tool messages (those without corresponding AI messages)\n     * regardless of whether editing is triggered\n     */\n    const orphanedIndices: number[] = [];\n    for (let i = 0; i < messages.length; i++) {\n      const msg = messages[i];\n      if (ToolMessage.isInstance(msg)) {\n        // Check if this tool message has a corresponding AI message\n        const aiMessage = this.#findAIMessageForToolCall(\n          messages.slice(0, i),\n          msg.tool_call_id\n        );\n\n        if (!aiMessage) {\n          // Orphaned tool message - mark for removal\n          orphanedIndices.push(i);\n        } else {\n          // Check if the AI message actually has this tool call\n          const toolCall = aiMessage.tool_calls?.find(\n            (call) => call.id === msg.tool_call_id\n          );\n          if (!toolCall) {\n            // Orphaned tool message - mark for removal\n            orphanedIndices.push(i);\n          }\n        }\n      }\n    }\n\n    /**\n     * Remove orphaned tool messages in reverse order to maintain indices\n     */\n    for (let i = orphanedIndices.length - 1; i >= 0; i--) {\n      messages.splice(orphanedIndices[i]!, 1);\n    }\n\n    /**\n     * Recalculate tokens after removing orphaned messages\n     */\n    let currentTokens = tokens;\n    if (orphanedIndices.length > 0) {\n      currentTokens = await countTokens(messages);\n    }\n\n    /**\n     * Check if editing should be triggered\n     */\n    if (!this.#shouldEdit(messages, currentTokens, model)) {\n      return;\n    }\n\n    /**\n     * Find all tool message candidates with their actual indices in the messages array\n     */\n    const candidates: { 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    if (candidates.length === 0) {\n      return;\n    }\n\n    /**\n     * Determine how many tool results to keep based on keep policy\n     */\n    const keepCount = await this.#determineKeepCount(\n      candidates,\n      countTokens,\n      model\n    );\n\n    /**\n     * Keep the most recent tool messages based on keep policy\n     */\n    const candidatesToClear =\n      keepCount >= candidates.length\n        ? []\n        : keepCount > 0\n          ? candidates.slice(0, -keepCount)\n          : candidates;\n\n    /**\n     * If clearAtLeast is set, we may need to clear more messages to meet the token requirement\n     * This is a deprecated feature that conflicts with keep, but we support it for backwards compatibility\n     */\n    let clearedTokens = 0;\n    const initialCandidatesToClear = [...candidatesToClear];\n\n    for (const { idx, msg: toolMessage } of initialCandidatesToClear) {\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, currentTokens - newTokenCount);\n    }\n\n    /**\n     * If clearAtLeast is set and we haven't cleared enough tokens,\n     * continue clearing more messages (going backwards from keepCount)\n     * This is deprecated behavior but maintained for backwards compatibility\n     */\n    if (this.clearAtLeast > 0 && clearedTokens < this.clearAtLeast) {\n      /**\n       * Find remaining candidates that weren't cleared yet (those that were kept)\n       */\n      const remainingCandidates =\n        keepCount > 0 && keepCount < candidates.length\n          ? candidates.slice(-keepCount)\n          : [];\n\n      /**\n       * Clear additional messages until we've cleared at least clearAtLeast tokens\n       * Go backwards through the kept messages\n       */\n      for (let i = remainingCandidates.length - 1; i >= 0; i--) {\n        if (clearedTokens >= this.clearAtLeast) {\n          break;\n        }\n\n        const { idx, msg: toolMessage } = remainingCandidates[i]!;\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, currentTokens - newTokenCount);\n      }\n    }\n  }\n\n  /**\n   * Determine whether editing should run for the current token usage\n   */\n  #shouldEdit(\n    messages: BaseMessage[],\n    totalTokens: number,\n    model: BaseLanguageModel\n  ): boolean {\n    /**\n     * Check each condition (OR logic between conditions)\n     */\n    for (const trigger of this.#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        if (!model) {\n          continue;\n        }\n        const maxInputTokens = getProfileLimits(model);\n        if (typeof maxInputTokens === \"number\") {\n          const threshold = Math.floor(maxInputTokens * trigger.fraction);\n          if (threshold <= 0) {\n            continue;\n          }\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          continue;\n        }\n      }\n\n      /**\n       * If condition has at least one property and all properties are satisfied, trigger editing\n       */\n      if (hasAnyProperty && conditionMet) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  /**\n   * Determine how many tool results to keep based on keep policy\n   */\n  async #determineKeepCount(\n    candidates: Array<{ idx: number; msg: ToolMessage }>,\n    countTokens: TokenCounter,\n    model: BaseLanguageModel\n  ): Promise<number> {\n    if (\"messages\" in this.keep && this.keep.messages !== undefined) {\n      return this.keep.messages;\n    }\n\n    if (\"tokens\" in this.keep && this.keep.tokens !== undefined) {\n      /**\n       * For token-based keep, count backwards from the end until we exceed the token limit\n       * This is a simplified implementation - keeping N most recent tool messages\n       * A more sophisticated implementation would count actual tokens\n       */\n      const targetTokens = this.keep.tokens;\n      let tokenCount = 0;\n      let keepCount = 0;\n\n      for (let i = candidates.length - 1; i >= 0; i--) {\n        const candidate = candidates[i];\n        /**\n         * Estimate tokens for this tool message (simplified - could be improved)\n         */\n        const msgTokens = await countTokens([candidate.msg]);\n        if (tokenCount + msgTokens <= targetTokens) {\n          tokenCount += msgTokens;\n          keepCount++;\n        } else {\n          break;\n        }\n      }\n\n      return keepCount;\n    }\n\n    if (\"fraction\" in this.keep && this.keep.fraction !== undefined) {\n      if (!model) {\n        return DEFAULT_KEEP;\n      }\n      const maxInputTokens = getProfileLimits(model);\n      if (typeof maxInputTokens === \"number\") {\n        const targetTokens = Math.floor(maxInputTokens * this.keep.fraction);\n        if (targetTokens <= 0) {\n          return DEFAULT_KEEP;\n        }\n        /**\n         * Use token-based logic with fractional target\n         */\n        let tokenCount = 0;\n        let keepCount = 0;\n\n        for (let i = candidates.length - 1; i >= 0; i--) {\n          const candidate = candidates[i];\n          const msgTokens = await countTokens([candidate.msg]);\n          if (tokenCount + msgTokens <= targetTokens) {\n            tokenCount += msgTokens;\n            keepCount++;\n          } else {\n            break;\n          }\n        }\n\n        return keepCount;\n      }\n    }\n\n    return DEFAULT_KEEP;\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-sonnet-4-5\",\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 * // Single condition: trigger if tokens >= 50000 AND messages >= 20\n * const agent1 = createAgent({\n *   model: \"anthropic:claude-sonnet-4-5\",\n *   tools: [searchTool, calculatorTool],\n *   middleware: [\n *     contextEditingMiddleware({\n *       edits: [\n *         new ClearToolUsesEdit({\n *           trigger: { tokens: 50000, messages: 20 },\n *           keep: { messages: 5 },\n *           excludeTools: [\"search\"],\n *           clearToolInputs: true,\n *         }),\n *       ],\n *       tokenCountMethod: \"approx\",\n *     }),\n *   ],\n * });\n *\n * // Multiple conditions: trigger if (tokens >= 50000 AND messages >= 20) OR (tokens >= 30000 AND messages >= 50)\n * const agent2 = createAgent({\n *   model: \"anthropic:claude-sonnet-4-5\",\n *   tools: [searchTool, calculatorTool],\n *   middleware: [\n *     contextEditingMiddleware({\n *       edits: [\n *         new ClearToolUsesEdit({\n *           trigger: [\n *             { tokens: 50000, messages: 20 },\n *             { tokens: 30000, messages: 50 },\n *           ],\n *           keep: { messages: 5 },\n *         }),\n *       ],\n *     }),\n *   ],\n * });\n *\n * // Fractional trigger with model profile\n * const agent3 = createAgent({\n *   model: chatModel,\n *   tools: [searchTool, calculatorTool],\n *   middleware: [\n *     contextEditingMiddleware({\n *       edits: [\n *         new ClearToolUsesEdit({\n *           trigger: { fraction: 0.8 },  // Trigger at 80% of model's max tokens\n *           keep: { fraction: 0.3 },     // Keep 30% of model's max tokens\n *           model: chatModel,\n *         }),\n *       ],\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 BaseLanguageModel & {\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      /**\n       * Apply each edit in sequence\n       */\n      for (const edit of edits) {\n        await edit.apply({\n          messages: request.messages,\n          model: request.model as BaseLanguageModel,\n          countTokens,\n        });\n      }\n\n      return handler(request);\n    },\n  });\n}\n"],"mappings":";;;;;;AA4BA,MAAM,2BAA2B;AACjC,MAAM,yBAAyB;AAC/B,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuLrB,IAAa,oBAAb,MAAsD;CACpD;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,SAAkC,EAAE,EAAE;EAEhD,IAAI,UAAmD,OAAO;AAC9D,MAAI,OAAO,kBAAkB,KAAA,GAAW;AACtC,WAAQ,KACN,yEACD;AACD,OAAI,YAAY,KAAA,EACd,WAAU,EAAE,QAAQ,OAAO,eAAe;;EAI9C,IAAI,OAA6B,OAAO;AACxC,MAAI,OAAO,iBAAiB,KAAA,GAAW;AACrC,WAAQ,KACN,uEACD;AACD,OAAI,SAAS,KAAA,EACX,QAAO,EAAE,UAAU,OAAO,cAAc;;AAK5C,MAAI,YAAY,KAAA,EACd,WAAU,EAAE,QAAQ,wBAAwB;AAE9C,MAAI,SAAS,KAAA,EACX,QAAO,EAAE,UAAU,cAAc;AAInC,MAAI,MAAM,QAAQ,QAAQ,EAAE;AAC1B,SAAA,oBAA0B,QAAQ,KAAK,MAAMC,sBAAAA,kBAAkB,MAAM,EAAE,CAAC;AACxE,QAAK,UAAU,MAAA;SACV;GACL,MAAM,YAAYA,sBAAAA,kBAAkB,MAAM,QAAQ;AAClD,SAAA,oBAA0B,CAAC,UAAU;AACrC,QAAK,UAAU;;EAIjB,MAAM,gBAAgBC,sBAAAA,WAAW,MAAM,KAAK;AAC5C,OAAK,OAAO;AAGZ,MAAI,OAAO,iBAAiB,KAAA,EAC1B,SAAQ,KACN,wMAGD;AAEH,OAAK,eAAe,OAAO,gBAAgB;AAE3C,OAAK,kBAAkB,OAAO,mBAAmB;AACjD,OAAK,eAAe,IAAI,IAAI,OAAO,gBAAgB,EAAE,CAAC;AACtD,OAAK,cAAc,OAAO,eAAe;;CAG3C,MAAM,MAAM,QAIM;EAChB,MAAM,EAAE,UAAU,OAAO,gBAAgB;EACzC,MAAM,SAAS,MAAM,YAAY,SAAS;;;;;EAM1C,MAAM,kBAA4B,EAAE;AACpC,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GACxC,MAAM,MAAM,SAAS;AACrB,OAAIC,yBAAAA,YAAY,WAAW,IAAI,EAAE;IAE/B,MAAM,YAAY,MAAA,yBAChB,SAAS,MAAM,GAAG,EAAE,EACpB,IAAI,aACL;AAED,QAAI,CAAC,UAEH,iBAAgB,KAAK,EAAE;aAMnB,CAHa,UAAU,YAAY,MACpC,SAAS,KAAK,OAAO,IAAI,aAC3B,CAGC,iBAAgB,KAAK,EAAE;;;;;;AAS/B,OAAK,IAAI,IAAI,gBAAgB,SAAS,GAAG,KAAK,GAAG,IAC/C,UAAS,OAAO,gBAAgB,IAAK,EAAE;;;;EAMzC,IAAI,gBAAgB;AACpB,MAAI,gBAAgB,SAAS,EAC3B,iBAAgB,MAAM,YAAY,SAAS;;;;AAM7C,MAAI,CAAC,MAAA,WAAiB,UAAU,eAAe,MAAM,CACnD;;;;EAMF,MAAM,aAAkD,EAAE;AAC1D,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GACxC,MAAM,MAAM,SAAS;AACrB,OAAIA,yBAAAA,YAAY,WAAW,IAAI,CAC7B,YAAW,KAAK;IAAE,KAAK;IAAG;IAAK,CAAC;;AAIpC,MAAI,WAAW,WAAW,EACxB;;;;EAMF,MAAM,YAAY,MAAM,MAAA,mBACtB,YACA,aACA,MACD;;;;EAKD,MAAM,oBACJ,aAAa,WAAW,SACpB,EAAE,GACF,YAAY,IACV,WAAW,MAAM,GAAG,CAAC,UAAU,GAC/B;;;;;EAMR,IAAI,gBAAgB;EACpB,MAAM,2BAA2B,CAAC,GAAG,kBAAkB;AAEvD,OAAK,MAAM,EAAE,KAAK,KAAK,iBAAiB,0BAA0B;AAOhE,QAHuB,YAAY,mBAAmB,kBAGlC,QAClB;;;;GAMF,MAAM,YAAY,MAAA,yBAChB,SAAS,MAAM,GAAG,IAAI,EACtB,YAAY,aACb;AAED,OAAI,CAAC,UACH;;;;GAMF,MAAM,WAAW,UAAU,YAAY,MACpC,SAAS,KAAK,OAAO,YAAY,aACnC;AAED,OAAI,CAAC,SACH;;;;GAMF,MAAM,WAAW,YAAY,QAAQ,SAAS;AAC9C,OAAI,KAAK,aAAa,IAAI,SAAS,CACjC;;;;AAMF,YAAS,OAAO,IAAIA,yBAAAA,YAAY;IAC9B,cAAc,YAAY;IAC1B,SAAS,KAAK;IACd,MAAM,YAAY;IAClB,UAAU,KAAA;IACV,mBAAmB;KACjB,GAAG,YAAY;KACf,iBAAiB;MACf,SAAS;MACT,UAAU;MACX;KACF;IACF,CAAC;;;;AAKF,OAAI,KAAK,iBAAiB;IACxB,MAAM,WAAW,SAAS,QAAQ,UAAU;AAC5C,QAAI,YAAY,EACd,UAAS,YAAY,MAAA,6BACnB,WACA,YAAY,aACb;;;;;GAOL,MAAM,gBAAgB,MAAM,YAAY,SAAS;AACjD,mBAAgB,KAAK,IAAI,GAAG,gBAAgB,cAAc;;;;;;;AAQ5D,MAAI,KAAK,eAAe,KAAK,gBAAgB,KAAK,cAAc;;;;GAI9D,MAAM,sBACJ,YAAY,KAAK,YAAY,WAAW,SACpC,WAAW,MAAM,CAAC,UAAU,GAC5B,EAAE;;;;;AAMR,QAAK,IAAI,IAAI,oBAAoB,SAAS,GAAG,KAAK,GAAG,KAAK;AACxD,QAAI,iBAAiB,KAAK,aACxB;IAGF,MAAM,EAAE,KAAK,KAAK,gBAAgB,oBAAoB;AAQtD,SAHuB,YAAY,mBAAmB,kBAGlC,QAClB;;;;IAMF,MAAM,YAAY,MAAA,yBAChB,SAAS,MAAM,GAAG,IAAI,EACtB,YAAY,aACb;AAED,QAAI,CAAC,UACH;;;;IAMF,MAAM,WAAW,UAAU,YAAY,MACpC,SAAS,KAAK,OAAO,YAAY,aACnC;AAED,QAAI,CAAC,SACH;;;;IAMF,MAAM,WAAW,YAAY,QAAQ,SAAS;AAC9C,QAAI,KAAK,aAAa,IAAI,SAAS,CACjC;;;;AAMF,aAAS,OAAO,IAAIA,yBAAAA,YAAY;KAC9B,cAAc,YAAY;KAC1B,SAAS,KAAK;KACd,MAAM,YAAY;KAClB,UAAU,KAAA;KACV,mBAAmB;MACjB,GAAG,YAAY;MACf,iBAAiB;OACf,SAAS;OACT,UAAU;OACX;MACF;KACF,CAAC;;;;AAKF,QAAI,KAAK,iBAAiB;KACxB,MAAM,WAAW,SAAS,QAAQ,UAAU;AAC5C,SAAI,YAAY,EACd,UAAS,YAAY,MAAA,6BACnB,WACA,YAAY,aACb;;;;;IAOL,MAAM,gBAAgB,MAAM,YAAY,SAAS;AACjD,oBAAgB,KAAK,IAAI,GAAG,gBAAgB,cAAc;;;;;;;CAQhE,YACE,UACA,aACA,OACS;;;;AAIT,OAAK,MAAM,WAAW,MAAA,mBAAyB;;;;GAI7C,IAAI,eAAe;GACnB,IAAI,iBAAiB;AAErB,OAAI,QAAQ,aAAa,KAAA,GAAW;AAClC,qBAAiB;AACjB,QAAI,SAAS,SAAS,QAAQ,SAC5B,gBAAe;;AAInB,OAAI,QAAQ,WAAW,KAAA,GAAW;AAChC,qBAAiB;AACjB,QAAI,cAAc,QAAQ,OACxB,gBAAe;;AAInB,OAAI,QAAQ,aAAa,KAAA,GAAW;AAClC,qBAAiB;AACjB,QAAI,CAAC,MACH;IAEF,MAAM,iBAAiBK,sBAAAA,iBAAiB,MAAM;AAC9C,QAAI,OAAO,mBAAmB,UAAU;KACtC,MAAM,YAAY,KAAK,MAAM,iBAAiB,QAAQ,SAAS;AAC/D,SAAI,aAAa,EACf;AAEF,SAAI,cAAc,UAChB,gBAAe;;;;;AAMjB;;;;;AAOJ,OAAI,kBAAkB,aACpB,QAAO;;AAIX,SAAO;;;;;CAMT,OAAA,mBACE,YACA,aACA,OACiB;AACjB,MAAI,cAAc,KAAK,QAAQ,KAAK,KAAK,aAAa,KAAA,EACpD,QAAO,KAAK,KAAK;AAGnB,MAAI,YAAY,KAAK,QAAQ,KAAK,KAAK,WAAW,KAAA,GAAW;;;;;;GAM3D,MAAM,eAAe,KAAK,KAAK;GAC/B,IAAI,aAAa;GACjB,IAAI,YAAY;AAEhB,QAAK,IAAI,IAAI,WAAW,SAAS,GAAG,KAAK,GAAG,KAAK;IAC/C,MAAM,YAAY,WAAW;;;;IAI7B,MAAM,YAAY,MAAM,YAAY,CAAC,UAAU,IAAI,CAAC;AACpD,QAAI,aAAa,aAAa,cAAc;AAC1C,mBAAc;AACd;UAEA;;AAIJ,UAAO;;AAGT,MAAI,cAAc,KAAK,QAAQ,KAAK,KAAK,aAAa,KAAA,GAAW;AAC/D,OAAI,CAAC,MACH,QAAO;GAET,MAAM,iBAAiBA,sBAAAA,iBAAiB,MAAM;AAC9C,OAAI,OAAO,mBAAmB,UAAU;IACtC,MAAM,eAAe,KAAK,MAAM,iBAAiB,KAAK,KAAK,SAAS;AACpE,QAAI,gBAAgB,EAClB,QAAO;;;;IAKT,IAAI,aAAa;IACjB,IAAI,YAAY;AAEhB,SAAK,IAAI,IAAI,WAAW,SAAS,GAAG,KAAK,GAAG,KAAK;KAC/C,MAAM,YAAY,WAAW;KAC7B,MAAM,YAAY,MAAM,YAAY,CAAC,UAAU,IAAI,CAAC;AACpD,SAAI,aAAa,aAAa,cAAc;AAC1C,oBAAc;AACd;WAEA;;AAIJ,WAAO;;;AAIX,SAAO;;CAGT,0BACE,kBACA,YACkB;AAElB,OAAK,IAAI,IAAI,iBAAiB,SAAS,GAAG,KAAK,GAAG,KAAK;GACrD,MAAM,MAAM,iBAAiB;AAC7B,OAAIC,yBAAAA,UAAU,WAAW,IAAI;QACP,IAAI,YAAY,MACjC,SAAS,KAAK,OAAO,WACvB,CAEC,QAAO;;;AAIb,SAAO;;CAGT,8BACE,SACA,YACW;EACX,MAAM,mBAAmB,QAAQ,YAAY,KAAK,aAAa;AAC7D,OAAI,SAAS,OAAO,WAClB,QAAO;IAAE,GAAG;IAAU,MAAM,EAAE;IAAE;AAElC,UAAO;IACP;EAEF,MAAM,WAAW,EAAE,GAAG,QAAQ,mBAAmB;EACjD,MAAM,eAAe,EACnB,GAAI,SAAS,iBACd;EAED,MAAM,aAAa,IAAI,IACrB,aAAa,oBACd;AACD,aAAW,IAAI,WAAW;AAC1B,eAAa,sBAAsB,MAAM,KAAK,WAAW,CAAC,MAAM;AAChE,WAAS,kBAAkB;AAE3B,SAAO,IAAIA,yBAAAA,UAAU;GACnB,SAAS,QAAQ;GACjB,YAAY;GACZ,mBAAmB;GACnB,IAAI,QAAQ;GACZ,MAAM,QAAQ;GACd,mBAAmB,QAAQ;GAC5B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkJN,SAAgB,yBACd,SAAyC,EAAE,EAC3C;CACA,MAAM,QAAQ,OAAO,SAAS,CAAC,IAAI,mBAAmB,CAAC;CACvD,MAAM,mBAAmB,OAAO,oBAAoB;AAEpD,QAAOC,mBAAAA,iBAAiB;EACtB,MAAM;EACN,eAAe,OAAO,SAAS,YAAY;AACzC,OAAI,CAAC,QAAQ,YAAY,QAAQ,SAAS,WAAW,EACnD,QAAO,QAAQ,QAAQ;;;;GAMzB,MAAM,YAAY,QAAQ,eACtB,CAAC,IAAIC,yBAAAA,cAAc,QAAQ,aAAa,CAAC,GACzC,EAAE;GAEN,MAAM,cACJ,qBAAqB,WACjBC,cAAAA,2BACA,OAAO,aAA6C;IAClD,MAAM,cAAc,CAAC,GAAG,WAAW,GAAG,SAAS;;;;;AAM/C,QAAI,8BAA8B,QAAQ,MACxC,QACE,QAAQ,MASP,yBAAyB,YAAY,CACrC,MAAM,EAAE,iBAAiB,WAAW;AAGzC,UAAM,IAAI,MACR,UAAU,QAAQ,MAAM,SAAS,CAAC,mCACnC;;;;;AAMT,QAAK,MAAM,QAAQ,MACjB,OAAM,KAAK,MAAM;IACf,UAAU,QAAQ;IAClB,OAAO,QAAQ;IACf;IACD,CAAC;AAGJ,UAAO,QAAQ,QAAQ;;EAE1B,CAAC"}