Compactación de sesiones con la API de Prompt

Publicado: 23 de junio de 2026

Cada sesión de LanguageModel tiene una ventana de contexto finita. A medida que una conversación crece, el modelo acumula el historial completo de mensajes en su contexto: cada instrucción del usuario y cada respuesta del asistente. Cuando se llena la ventana, se activa el control automático de desbordamiento del navegador. Desaloja los pares de mensajes más antiguos, un par de instrucción y respuesta a la vez, para liberar espacio para la nueva instrucción. Si la instrucción entrante es tan grande que no cabe incluso después de quitar todo el historial de conversación, la llamada falla directamente con un QuotaExceededError.

La compactación de sesiones es una alternativa proactiva: resume el historial de conversaciones con la API de Summarizer y, luego, reinicia una nueva sesión con esos resúmenes como initialPrompts. El navegador nunca expulsa a initialPrompts durante el control de desbordamiento del tiempo de ejecución, por lo que el resumen compactado permanece anclado de forma permanente en el contexto del modelo, siempre y cuando los resúmenes en sí quepan dentro de la ventana de contexto cuando se llama a create(). La nueva sesión mantiene el mismo hilo de conversación a una fracción del costo original en tokens.

La compactación de sesiones permite que las conversaciones LanguageModel de larga duración se mantengan dentro de la ventana de contexto sin perder continuidad. Los pasos clave son los siguientes:

  1. Supervisa contextUsage en relación con contextWindow y muéstrale el resultado al usuario.
  2. Escucha el evento contextoverflow como una advertencia anticipada.
  3. Detecta el idioma de cada mensaje con la API de Language Detector y, luego, resúmelo con una instancia de la API de Summarizer que tenga en cuenta el idioma.
  4. Destruye la sesión anterior y crea una nueva con initialPrompts.
  5. Conserva una copia de fullHistory para la recuperación de errores.

Realiza un seguimiento del uso del contexto

La API de Prompt expone dos atributos para supervisar qué tan completo es el contexto de una sesión:

  • session.contextUsage: Es la cantidad de tokens que se consumieron actualmente.
  • session.contextWindow: Es la capacidad total de tokens de la sesión.

Refleja esto en un elemento <progress> para que los usuarios sepan de un vistazo qué tan cerca está la sesión de su límite. Establece value y max directamente en los recuentos de tokens. El navegador ajusta la barra automáticamente:

<progress id="token-bar" value="0" max="1"></progress>
<label for="token-bar" id="token-label">Context: — / — tokens</label>
function updateTokenDisplay(session) {
  const usage = session.contextUsage;
  const total = session.contextWindow;

  tokenBar.value = usage;
  tokenBar.max = total;
  tokenLabel.textContent =
    `${Math.round(usage)} / ${Math.round(total)} tokens ` +
    `(${Math.round((usage / total) * 100)}%)`;
}

Llama a updateTokenDisplay() después de cada respuesta a la instrucción para que la barra se mantenga actualizada.

Cómo detectar el desbordamiento de contexto

Cuando una instrucción nueva supera el contexto restante, comienza la recuperación automática del navegador: quita los pares de instrucciones y respuestas más antiguos de a uno por vez hasta que libera suficiente espacio. El evento contextoverflow se activa en el momento en que comienza esta expulsión. Registra un controlador inmediatamente después de crear la sesión:

session.addEventListener('contextoverflow', () => {
  showWarning('⚠ Context window nearly full. Consider compacting the session.');
});

Hay dos propiedades importantes de este comportamiento de descarte:

  • initialPrompts no se descartan en el tiempo de ejecución. El navegador no los quita para dejar espacio para una instrucción entrante. Sin embargo, si el tamaño combinado de la initialPrompts que se pasa a LanguageModel.create() es demasiado grande para caber en la ventana de contexto, create() rechaza la solicitud con un QuotaExceededError, por lo que debes asegurarte de que la compactación sea lo suficientemente pequeña como para continuar la conversación.
  • La expulsión tiene un límite. Si la instrucción entrante es tan grande que quitar toda la conversación anterior no es suficiente, la llamada a prompt() o promptStreaming() falla con un QuotaExceededError y no se quita nada.

Obtén más información sobre el control del desbordamiento de contexto en la documentación de la API de Prompt.

Usa el evento contextoverflow para advertir al usuario, inhabilitar el botón de envío o activar la compactación automáticamente antes de que el navegador comience a descartar de forma silenciosa el historial de conversaciones.

Cómo compactar la sesión

La compactación tiene tres pasos:

  1. Resume cada mensaje del historial de conversación con la API de Summarizer.
  2. Destruye la sesión anterior.
  3. Crea una sesión nueva con los resúmenes como initialPrompts.

Resume el historial

La API de Summarizer es ideal para comprimir mensajes de chat individuales. Para cada mensaje, primero detecta su idioma con la API de Language Detector para que el resumidor se pueda configurar correctamente:

async function detectLanguage(text, threshold = 0.7) {
  const detector = await LanguageDetector.create();
  const results = await detector.detect(text);
  if (results.length > 0 && results[0].confidence >= threshold) {
    return results[0].detectedLanguage;
  }
  return null; // confidence too low — caller falls back to navigator.language
}

El 0.7 umbral de confianza evita actuar ante detecciones inciertas. Cuando la confianza sea inferior al umbral, recurre a navigator.language.

A continuación, crea un resumidor configurado para el idioma detectado. Prefiere preference: 'speed' para seleccionar la variante de modelo más pequeña y con menor latencia, y recurre a preference: 'auto' si el modelo más rápido no admite el idioma detectado:

const summarizers = {}; // cache, keyed by `${format}:${lang}`

async function getSummarizer(format, lang) {
  const key = `${format}:${lang}`;
  if (summarizers[key]) return summarizers[key];

  const baseOptions = {
    type: 'tldr',
    format, // 'markdown' or 'plain-text'
    length: 'short',
    expectedInputLanguages: [lang],
    expectedContextLanguages: [lang],
    outputLanguage: lang,
  };

  let options = { ...baseOptions, preference: 'speed' };
  let avail = await Summarizer.availability(options);

  if (avail === 'unavailable') {
    options = { ...baseOptions, preference: 'auto' };
    avail = await Summarizer.availability(options);
  }

  if (avail === 'unavailable') {
    throw new Error('Summarizer API unavailable on this device.');
  }

  summarizers[key] = await Summarizer.create(options);
  return summarizers[key];
}

El almacenamiento en caché de los resumidores por par format+lang evita llamadas create() redundantes cuando los mensajes consecutivos comparten el mismo idioma.

El argumento format se deriva del contenido del mensaje. Si especificas 'markdown' para la prosa simple, se puede introducir un formato no deseado, y si especificas 'plain-text' para Markdown, se quitan los delimitadores de código y el énfasis. Una pequeña expresión regular distingue las dos:

function looksLikeMarkdown(text) {
  return /(?:^#{1,6} |^[-*+] |\d+\. |\*\*|__|\[.+?\]\(|^> |^```)/m.test(text);
}

Una vez que se resuelven el idioma y el formato, se resume cada mensaje y se pasa una cadena context para que el modelo comprenda que está comprimiendo un turno de chat, no un documento independiente:

const compacted = [];

for (const msg of history) {
  const lang = (await detectLanguage(msg.content)) ?? navigator.language;
  const format = looksLikeMarkdown(msg.content) ? 'markdown' : 'plain-text';
  const summarizer = await getSummarizer(format, lang);

  const summary = await summarizer.summarize(msg.content.trim(), {
    context:
      `This is a ${msg.role} turn from a chat conversation. ` +
      `Preserve its key meaning as concisely as possible.`,
  });

  // Only use the summary if it's actually shorter.
  compacted.push({
    role: msg.role,
    content:
      summary.trim().length < msg.content.length ? summary.trim() : msg.content,
  });
}

Destruye la sesión anterior

Libera los recursos de la sesión anterior antes de crear el reemplazo:

session.destroy();
session = null;

Crea una sesión nueva con el historial compactado

Pasa los mensajes compactados como initialPrompts para inicializar la nueva sesión con el contexto de la conversación:

// Collect every language the detector was confident about.
const sessionLangs =
  confidentLangs.size > 0 ? [...confidentLangs] : [navigator.language];

session = await LanguageModel.create({
  expectedInputs: [{ type: 'text', languages: sessionLangs }],
  expectedOutputs: [{ type: 'text', languages: sessionLangs }],
  initialPrompts: compacted,
});

// Re-register the overflow handler on the new session.
session.addEventListener('contextoverflow', () => {
  /* ... */
});

La nueva sesión comienza con un valor de contextUsage más bajo. La conversación continúa desde donde se quedó: el modelo tiene los resúmenes como contexto previo, por lo que puede responder preguntas de seguimiento sobre temas anteriores.

Soluciona errores

Si falla la creación del resumen o de la sesión después de que se destruyó la sesión anterior, el usuario no podrá chatear. Mantén un array fullHistory separado que nunca se sobrescriba por la compactación y úsalo como alternativa de recuperación:

const history = []; // current session's view, replaced on each compaction
const fullHistory = []; // every original message, never overwritten

// In the catch block:
if (!session) {
  session = await LanguageModel.create({
    initialPrompts: fullHistory.map(({ role, content }) => ({ role, content })),
  });
  session.addEventListener('contextoverflow', () => {
    /* ... */
  });
}

La recuperación de fullHistory puede volver a colocar el contexto cerca de su capacidad, pero el usuario al menos vuelve a un estado de funcionamiento y puede intentar otra compactación de inmediato.

Cómo evitar que se comprima cierto contenido (opcional)

Si hay partes críticas de un mensaje que siempre deben permanecer en el contexto, por ejemplo, muestras de código, procésalas por separado. En el siguiente ejemplo, se divide un mensaje en segmentos alternados de prosa y vallas de código, y, luego, solo se resumen las partes de prosa, mientras que los segmentos de código se dejan intactos:

// Splits text into alternating prose and code-fence segments.
// Returns [{ type: 'prose'|'code', content: string }, …]
function splitByCodeFences(text) {
  const parts = [];
  const re = /^```[^\n]*\n[\s\S]*?^```[ \t]*$/gm;
  let lastIndex = 0;
  let match;
  while ((match = re.exec(text)) !== null) {
    if (match.index > lastIndex) {
      parts.push({
        type: 'prose',
        content: text.slice(lastIndex, match.index),
      });
    }
    parts.push({ type: 'code', content: match[0] });
    lastIndex = match.index + match[0].length;
  }
  if (lastIndex < text.length) {
    parts.push({ type: 'prose', content: text.slice(lastIndex) });
  }
  return parts;
}

Probar demostración

La demostración de compactación de sesiones te permite chatear con la API de Prompt y compactar la sesión en cualquier momento. La barra de tokens muestra el uso del contexto en tiempo real y cambia de color a medida que se completa el contexto. Después de cada compactación, una entrada de registro registra los recuentos de tokens antes y después para que puedas observar directamente la reducción.

Puedes inspeccionar el JSON de conversación completo y compacto en la sección Debug: conversation JSON plegable en la parte inferior de la página.

El código fuente está en GitHub.