Skip to content

Évolution de la compaction (compaction) du contexte

源码版本rust-v0.145.0

codex traite l'historique de conversation (conversation history) comme une ressource finie. Quand les tokens cumulés approchent de la fenêtre de contexte du modèle, il faut replier l'ancien historique en un résumé pour libérer de la place et continuer au tour suivant. Ce chemin de compaction (compaction) dans core/src/compact*.rs a été réécrit plusieurs fois : version locale où le modèle produit lui-même le résumé, puis remote compaction côté server, puis évolution v2. Les trois implémentations partagent le même hook et le même lifecycle analytics, mais la source du « résumé » est très différente.

Responsabilités

  1. Décider quand déclencher la compaction : trois sources — auto (seuil de tokens atteint), manual (l'utilisateur tape /compact), ou CompactionReason (downshift du modèle, changement de comp hash, etc.) — dispatchées par run_inline_auto_compact_task et run_compact_task.
  2. Tourner un turn de résumé : la version locale rappelle la Responses API pour obtenir le résultat du SUMMARIZATION_PROMPT ; la version remote fait que le server renvoie directement un ResponseItem::Compaction ; v2 exige en plus que le server retourne strictement un seul compaction item.
  3. Construire l'historique de remplacement : résumé + derniers messages utilisateur assemblés en nouvel historique, l'ancien historique est jeté ; build_compacted_history_with_limit tailore les messages utilisateur sur une limite de 20k tokens.
  4. Maintenir la fenêtre de contexte (context window) : chaque compaction ouvre une nouvelle fenêtre, via advance_auto_compact_window / start_new_context_window on met à jour window_id, previous_window_id, pour le comptage de tokens et le rollout trace.
  5. Exposer hooks et analytics : PreCompactHookOutcome / PostCompactHookOutcome laissent un plugin bloquer la compaction ; CompactionAnalyticsAttempt reporte trigger, reason, implementation et phase de chaque tentative.

Motivations de conception

La première compaction locale (compact.rs) consistait à « ce que le client renvoie une requête au modèle pour faire le résumé ». Le problème : le résumé lui-même consomme des tokens, et sa qualité dépend de la façon dont le modèle exécute SUMMARIZATION_PROMPT. Quand le provider de modèle a commencé à supporter nativement la compaction côté server, compact_remote.rs est apparu : en une seule requête Responses, le server renvoie directement l'item Compaction déjà replié, le client ne participe pas à la génération du résumé.

Mais v1 remote envoie tout l'historique au server, qui ne renvoie qu'un résumé ; le client doit lui-même re-tailler les sorties de function call. v2 (compact_remote_v2.rs) resserre ce flux : le server ne fait pas que renvoyer le résumé, il décide aussi des messages à conserver (retained messages) ; le client collecte juste le seul item Compaction attendu, et en cas d'erreur c'est fatal. v2 introduit aussi la limite RETAINED_MESSAGE_TOKEN_BUDGET = 64_000 pour empêcher les messages conservés de saturer à leur tour le contexte.

compact_token_budget.rs est une autre branche : il saute la génération de résumé et ouvre directement une nouvelle fenêtre. Ce chemin est réservé aux scénarios « on ne veut pas payer un appel de modèle pour le résumé » — par exemple quand l'utilisateur dit explicitement « vider le contexte ». Il traverse quand même le lifecycle complet des hooks et du turn item ContextCompaction, mais sans appeler le modèle.

Fichiers clés

codex-rs/core/src/compact.rs:150-219run_compact_task_inner, entrée principale de la compaction locale, avec wrapper pre/post hook et analytics.codex-rs/core/src/compact.rs:221-378run_compact_task_inner_impl, stream le modèle, retry, puis appelle replace_compacted_history.codex-rs/core/src/compact.rs:602-663build_compacted_history_with_limit, pousse les messages utilisateur depuis la fin jusqu'à la limite de 20k tokens.codex-rs/core/src/compact_remote.rs:76-107run_remote_compact_task, entrée remote v1, marquée ResponsesCompact.codex-rs/core/src/compact_remote_v2.rs:85-115run_remote_compact_task version v2, marquée ResponsesCompactionV2.codex-rs/core/src/compact_remote_v2.rs:385-443collect_compaction_output, impose que le server ne renvoie qu'un seul compaction item, sinon fatal.codex-rs/core/src/compact_remote_v2_attempt.rs:32-142run_remote_compact_v2_attempt, fait d'abord trim_function_call_history_to_fit_context_window puis envoie la requête.codex-rs/core/src/compact_model_fallback.rs:8-19should_retry_with_current_model, définit quelles erreurs méritent un retry avec changement de modèle.codex-rs/core/src/compact_token_budget.rs:64-90 — compaction par token budget, saute le résumé et ouvre une nouvelle fenêtre directement.

InitialContextInjection est une énum qui paraît accessoire mais s'avère critique : elle décide si l'initial context doit être inséré dans le nouvel historique après compaction. La compaction pre-turn / manual utilise DoNotInject, pour laisser le turn suivant réinjecter lui-même ; la compaction mid-turn doit utiliser BeforeLastUserMessage, car le modèle est entraîné à considérer que « après le résumé vient le dernier message » — il faut insérer le contexte avant le dernier vrai message utilisateur.

rust
// compact.rs:56-69 — mid-turn 与 pre-turn 压缩的上下文注入策略
#[derive(Debug)]
pub(crate) enum InitialContextInjection {
    BeforeLastUserMessage(Arc<WorldState>),
    DoNotInject,
}

Le cœur de la compaction locale envoie l'historique complet + SUMMARIZATION_PROMPT au modèle, et à la fin du stream prend le dernier message assistant comme summary. Attention, elle ne prend pas la sortie du modèle comme nouvel historique brut : elle l'enveloppe d'un préfixe format!("{SUMMARY_PREFIX}\n{summary_suffix}"), pour que is_summary_message puisse l'identifier plus tard.

rust
// compact.rs:323-336 — 从模型 stream 结果里取出摘要并打标
let history_snapshot = sess.clone_history().await;
let history_items = history_snapshot.raw_items();
let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let user_messages = collect_user_messages(history_items);

let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);

La contrainte clé de v2 remote est que le server doit retourner exactement un ResponseItem::Compaction, sinon c'est fatal :

rust
// compact_remote_v2.rs:419-434 — server 返回多条 compaction 视为协议错误
if !saw_completed {
    return Err(CodexErr::Stream(
        "remote compaction v2 stream closed before response.completed".to_string(),
        None,
    ));
}
if compaction_count != 1 {
    return Err(CodexErr::Fatal(format!(
        "remote compaction v2 expected exactly one compaction output item, got {compaction_count} from {output_item_count} output items"
    )));
}

Flux de données

Limites et échecs

  • Auto-guérison de ContextWindowExceeded : si le prompt du résumé fait exploser lui-même la fenêtre lors d'une compaction locale, on boucle sur history.remove_first_item() pour tronquer les plus anciens entries puis on retry, jusqu'à ce qu'il ne reste qu'un seul entry ; si ça explose encore, on remonte une erreur (codex-rs/core/src/compact.rs:285-300).
  • Budget de retry stream plus serré : v2 remote clampe le nombre de retries à MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES = 2, parce que la compaction est déjà longue à exécuter ; réutiliser le budget de retry du stream générique ferait trop attendre (codex-rs/core/src/compact_remote_v2.rs:54-57).
  • Fallback de modèle : les erreurs InvalidRequest, ContextWindowExceeded, ServerOverloaded déclenchent should_retry_with_current_model, qui réessaie en changeant vers le modèle principal courant, pour éviter qu'une spécificité de provider ne bloque la compaction (codex-rs/core/src/compact_model_fallback.rs:8-19).
  • Position d'injection mid-turn : dans le nouvel historique après résumé, l'initial context ne peut pas être simplement pushé à la fin ; il faut le splicer avant le dernier real user message ; s'il n'y en a pas, l'insérer avant le summary, pour que le summary reste toujours en dernière position (codex-rs/core/src/compact.rs:542-587).
  • Compaction token-budget sans résumé : passe par start_new_context_window pour changer de fenêtre sans appeler le modèle, mais émet quand même un turn item ContextCompaction, pour que les hooks et l'UI ne ratent pas l'événement à cause d'un chemin différent (codex-rs/core/src/compact_token_budget.rs:76-82).

Récapitulatif

La complexité de la compaction vient surtout de la « coexistence de trois implémentations » : stream local, remote v1, remote v2, chacune avec son entry et son inner_impl, mais partageant le squelette CompactionAnalyticsAttempt, hooks, InitialContextInjection. Pour ajouter une fonctionnalité, vérifiez d'abord quel chemin le provider emprunte — should_use_remote_compact_task est le branchement — puis lisez le fichier correspondant. Pour voir comment l'appel au modèle lui-même est émis, enchaînez sur Client LLM et Responses API ; pour voir comment le résultat de la compaction est stocké dans l'historique, voir Thread de session et historique des messages.