Skip to content

ContextManager: estrategia de truncado de la ventana de contexto

源码版本v4.0.10

Responsabilidades

ContextManager es el truncador y optimizador que Cline invoca cuando «la ventana de contexto (context) está a punto de desbordarse». Cuando el total de tokens de una petición LLM se acerca al límite del modelo, el Task principal o SubagentRunner llama a sus métodos para eliminar un tramo de los mensajes antiguos de la conversación, conservando a la vez las últimas rondas, todas las definiciones de herramientas y los mensajes más antiguos marcados como «importantes». No almacena los mensajes originales —esos siguen en el arreglo apiConversationHistory—, sino que mantiene un intervalo conversationHistoryDeletedRange: [start, end] que indica al llamador «este rango de índices ya es inválido», además de un map in-memory contextHistoryUpdates que registra las modificaciones de sobreescritura sobre el contenido de los mensajes.

Su posición en la arquitectura de Cline se sitúa entre «el historial de mensajes» y «el cuerpo de la petición que se envía a la API». En cada ronda, el Task principal usa getTruncatedMessages (getTruncatedMessages:344) para cortar el arreglo history en la versión que se envía; al terminar la petición y detectar que el conteo de tokens es crítico, recurre a getNewContextMessagesAndMetadata (getNewContextMessagesAndMetadata:227) para decidir si hace falta recortar más. Por tanto, ContextManager es un pequeño módulo funcional con estado interno al que el agent loop principal consulta para decidir «qué enviar en la siguiente ronda».

Motivación de diseño

  • Solo borra un rango, no modifica el arreglo original: mantener [start, end] en lugar de hacer splice sobre el arreglo deja el coste del rollback de checkpoint en O(1) (getNextTruncationRange:299).
  • Siempre conserva los dos primeros mensajes: index 0 y 1 son el «par núcleo user/assistant»; tras borrar, el siguiente mensaje debe seguir siendo assistant para preservar la estructura alternante user-assistant (preserve pairing:331).
  • Cuatro estrategias de retención: none / lastTwo / half / quarter, según cuánto se borra. Cuantos más tokens se exceden, menos se conserva (keep strategies:309).
  • Umbral dinámico: si totalTokens / 2 ya supera maxAllowedSize, se va directo a quarter, cubriendo el escenario de conmutar de un modelo de 200k a uno de 64k (quarter fallback:252).
  • Optimización de file read antes del truncado: primero comprime los grandes bloques tool_result de lectura de archivos con «marca + restauración bajo demanda»; si ahorra más del 30%, evita el truncado (attemptFileReadOptimizationCore:626).
  • Ordenamiento por timestamp para rollback: contextHistoryUpdates se guarda como un arreglo [timestamp, updateType, update] y se busca el cutoff de derecha a izquierda por tiempo, permitiendo deshacer con precisión las modificaciones posteriores a cierto punto (truncate by timestamp:573).
  • Nueva ruta auto-condense: los modelos next-gen usan auto-condense con umbral 0.75, mientras los antiguos siguen juzgando por maxAllowedSize (thresholdPercentage:163).

Archivos clave

Flujo de datos

Bajo el flujo del Task principal, getNewContextMessagesAndMetadata es el punto de decisión clave. Entra, lee el conteo de tokens de la petición anterior a la API, lo compara con maxAllowedSize y solo actúa si se excede:

typescript
// apps/vscode/src/core/context/context-management/ContextManager.ts
if (previousApiReqIndex >= 0) {
  const previousRequestText = clineMessages[previousApiReqIndex]?.text
  if (previousRequestText) {
    const timestamp = clineMessages[previousApiReqIndex].ts
    const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequestText)
    const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
    const { maxAllowedSize } = getContextWindowInfo(api)

    if (totalTokens >= maxAllowedSize) {
      // Al cambiar de modelo, half puede no bastar; si token/2 supera maxAllowedSize, se va directo a quarter
      const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"

      let { anyContextUpdates, needToTruncate } = this.attemptFileReadOptimizationCore(
        apiConversationHistory,
        conversationHistoryDeletedRange,
        timestamp,
      )

      if (needToTruncate) {
        anyContextUpdates = this.applyStandardContextTruncationNoticeChange(timestamp) || anyContextUpdates
        conversationHistoryDeletedRange = this.getNextTruncationRange(
          apiConversationHistory,
          conversationHistoryDeletedRange,
          keep,
        )
        updatedConversationHistoryDeletedRange = true
      }

      if (anyContextUpdates) {
        await this.saveContextHistory(taskDirectory)
      }
    }
  }
}

const truncatedConversationHistory = this.getAndAlterTruncatedMessages(
  apiConversationHistory,
  conversationHistoryDeletedRange,
)

Esto se encuentra cerca de main truncation logic:240. Tras calcular el rango de truncado, getAndAlterTruncatedMessages recorre el history desde deletedRange[1] + 1 y aplica applyContextHistoryUpdates (applyContextHistoryUpdates:362) para incorporar las modificaciones de sobreescritura (optimización de file read / aviso de truncado estándar). Luego ensureToolResultsFollowToolUse hace una corrección de emparejamiento, garantizando que cada tool_use vaya seguido de su tool_result.

Límites y fallos

  • Con menos de 1 mensaje no se procesa: getAndAlterTruncatedMessages devuelve el arreglo original sin tocar (early return:358).
  • Si deletedRange no cambia, no se recalcula: compactConversationForContextWindow detecta si el nuevo rango es idéntico al anterior y sale temprano (range equal early return:929), evitando reescrituras innecesarias.
  • El final del rango debe ser assistant: tras borrar un tramo, el siguiente mensaje debe ser assistant; si no, rangeEndIndex -= 1 retrocede una posición (assistant pairing:333).
  • Si la optimización de file read no ahorra 30%, se sigue truncando: cuando percentSaved < 0.3, se asigna needToTruncate: true (30 percent threshold:654).
  • Fallo al escribir en saveContextHistory solo se loguea: el try/catch envuelve fs.writeFile y no propaga la excepción al flujo principal (save error handling:142).
  • Distintos modelos dejan distinto buffer: si contextWindow <= 64k, se aplica la fórmula del 80%, evitando que el buffer de modelos pequeños quede demasiado fino (deepseek buffer:31).
  • El rollback de checkpoint va por timestamp: truncateContextHistory no toca deletedRange; solo borra los updates posteriores al timestamp indicado, permitiendo retroceder el histórico a cualquier checkpoint (truncateContextHistory:552).

Resumen

ContextManager es el pilar que mantiene las conversaciones largas de Cline sin colapsar. Para ver cómo lo usa SubagentRunner, consulta subagente (subagent); para ver cómo el Task principal lo invoca dentro de la recursión para decidir el history de la siguiente ronda, consulta agent-loop/task-class; para el directorio del task y la persistencia relacionada, consulta storage/state-manager.

Véase la documentación oficial: Cline 文档 · README