Compression de session avec l'API Prompt

Publié le : 23 juin 2026

Chaque session LanguageModel dispose d'une fenêtre de contexte finie. Au fur et à mesure qu'une conversation se développe, le modèle accumule l'historique complet des messages dans son contexte : chaque requête utilisateur et chaque réponse de l'assistant. Lorsque la fenêtre est pleine, la gestion automatique du débordement du navigateur se déclenche. Il supprime les paires de messages les plus anciennes, une paire de requête et de réponse à la fois, pour faire de la place à la nouvelle requête. Si la requête entrante est si volumineuse que même la suppression de l'intégralité de l'historique des conversations ne suffit pas, l'appel échoue complètement avec un QuotaExceededError.

La compression de session est une alternative proactive : résumez l'historique des conversations avec l'API Summarizer, puis redémarrez une nouvelle session en utilisant ces résumés comme initialPrompts. Le navigateur n'expulse jamais initialPrompts lors de la gestion du dépassement de capacité d'exécution. Le résumé compacté reste donc ancré de manière permanente dans le contexte du modèle, tant que les résumés eux-mêmes tiennent dans la fenêtre de contexte lorsque create() est appelé. La nouvelle session reprend le même fil de discussion à un coût en jetons bien inférieur à celui d'origine.

La compression de session permet aux conversations LanguageModel de longue durée de rester dans la fenêtre de contexte sans perdre en continuité. Voici les principales étapes :

  1. Surveillez contextUsage par rapport à contextWindow et présentez-le à l'utilisateur.
  2. Écoutez l'événement contextoverflow comme avertissement anticipé.
  3. Détectez la langue de chaque message avec l'API Language Detector, puis résumez-le avec une instance de l'API Summarizer adaptée à la langue.
  4. Détruisez l'ancienne session et créez-en une nouvelle avec initialPrompts.
  5. Conservez une copie fullHistory pour la récupération en cas d'erreur.

Suivre l'utilisation du contexte

L'API Prompt expose deux attributs permettant de surveiller le niveau de remplissage du contexte d'une session :

  • session.contextUsage : nombre de jetons actuellement consommés.
  • session.contextWindow : capacité totale en jetons de la session.

Indiquez-le dans un élément <progress> pour que les utilisateurs sachent en un coup d'œil à quel point la session est proche de sa limite. Définissez directement value et max sur le nombre de jetons. Le navigateur met automatiquement la barre à l'échelle :

<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)}%)`;
}

Appelez updateTokenDisplay() après chaque réponse à une requête pour que la barre reste à jour.

Écouter le dépassement de contexte

Lorsqu'une nouvelle requête dépasse le contexte restant, la récupération automatique du navigateur commence : elle supprime les paires requête/réponse les plus anciennes une par une jusqu'à ce qu'elle libère suffisamment d'espace. L'événement contextoverflow se déclenche au début de cette éviction. Enregistrez un gestionnaire immédiatement après avoir créé la session :

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

Deux propriétés importantes caractérisent ce comportement d'éviction :

  • Les initialPrompts ne sont pas supprimés lors de l'exécution. Le navigateur ne les supprime pas pour faire de la place à une requête entrante. Toutefois, si la taille combinée des initialPrompts transmis à LanguageModel.create() est elle-même trop grande pour tenir dans la fenêtre de contexte, create() est rejeté avec un QuotaExceededError. Assurez-vous donc que la compression est suffisamment petite pour poursuivre la conversation.
  • L'éviction est limitée. Si la requête entrante est si volumineuse que même la suppression de l'intégralité de la conversation précédente ne suffit pas, l'appel prompt() ou promptStreaming() échoue avec un QuotaExceededError et rien n'est supprimé.

Pour en savoir plus sur la gestion du débordement de contexte, consultez la documentation de l'API Prompt.

Utilisez l'événement contextoverflow pour avertir l'utilisateur, désactiver le bouton d'envoi ou déclencher automatiquement la compression avant que le navigateur ne commence à supprimer silencieusement l'historique des conversations.

Réduire la session

La compaction comporte trois étapes :

  1. Résumez chaque message de l'historique des conversations avec l'API Summarizer.
  2. Détruisez l'ancienne session.
  3. Créez une session avec les résumés comme initialPrompts.

Résume l'historique

L'API Summarizer est idéale pour compresser des messages de chat individuels. Pour chaque message, commencez par détecter sa langue avec l'API Language Detector afin que le résumeur puisse être configuré correctement :

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
}

Le seuil de confiance 0.7 permet d'éviter d'agir sur des détections incertaines. Lorsque le niveau de confiance est inférieur au seuil, revenez à navigator.language.

Créez ensuite un résumeur configuré pour la langue détectée. Préférez preference: 'speed' pour sélectionner la variante de modèle plus petite et à latence plus faible, et revenez à preference: 'auto' si le modèle plus rapide ne prend pas en charge la langue détectée :

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];
}

La mise en cache des résumés par paire format+lang évite les appels create() redondants lorsque des messages consécutifs partagent la même langue.

L'argument format est dérivé du contenu du message lui-même. Si vous spécifiez 'markdown' pour du texte brut, cela peut entraîner une mise en forme indésirable. Si vous spécifiez 'plain-text' pour Markdown, cela supprime les clôtures de code et les mises en emphase. Une petite expression régulière permet de les distinguer :

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

Une fois la langue et le format définis, résumez chaque message et transmettez une chaîne context pour que le modèle comprenne qu'il compresse un tour de discussion, et non un document autonome :

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,
  });
}

Détruire l'ancienne session

Libérez les ressources de l'ancienne session avant de créer la session de remplacement :

session.destroy();
session = null;

Créer une session avec un historique compacté

Transmettez les messages compactés en tant que initialPrompts pour amorcer la nouvelle session avec le contexte de la conversation :

// 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 nouvelle session commence à un contextUsage inférieur. La conversation reprend là où elle s'était arrêtée : le modèle dispose des résumés comme contexte préalable, ce qui lui permet de répondre aux questions complémentaires sur les sujets précédents.

Gérer les erreurs

Si la création de résumés ou de sessions échoue après la destruction de l'ancienne session, l'utilisateur ne peut plus discuter. Conservez un tableau fullHistory distinct qui n'est jamais écrasé par la compaction et utilisez-le comme solution de secours pour la récupération :

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 récupération à partir de fullHistory peut à nouveau placer le contexte à proximité de sa capacité maximale, mais l'utilisateur est au moins de retour dans un état de fonctionnement et peut immédiatement essayer une autre compression.

Empêcher éventuellement la compression de certains contenus

Si un message contient des éléments essentiels qui doivent toujours rester dans le contexte (par exemple, des exemples de code), traitez-les séparément. L'exemple suivant divise un message en segments alternatifs de prose et de clôtures de code, puis ne résume que les parties de prose tout en laissant les segments de code intacts :

// 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;
}

Essayer la démo

La démonstration de compression de session vous permet de discuter avec l'API Prompt et de compresser la session à tout moment. La barre de jetons affiche l'utilisation du contexte en temps réel et change de couleur à mesure que le contexte se remplit. Après chaque compression, une entrée de journal enregistre le nombre de jetons avant et après, ce qui vous permet d'observer directement la réduction.

Vous pouvez inspecter le code JSON complet et compacté de la conversation dans la section réductible Déboguer : JSON de la conversation en bas de la page.

Le code source se trouve sur GitHub.