Skip to content

Context Engine: abstraction de compression de contexte

源码版本v2026.6.11

Responsabilités

Context Engine est la couche d'abstraction du cycle de vie du contexte agent (context): qui est en charge d'ingérer les messages, qui assemble le prompt, qui compresse (compact), qui maintient (maintain) le transcript — toutes ces décisions sont sorties de la boucle principale de l'agent et deviennent quelques méthodes de l'interface ContextEngine.

OpenClaw embarque un LegacyContextEngine comme implémentation par défaut, qui délègue tout le travail aux chemins préexistants (SessionManager pour la persistance, attempt.ts pour l'assemblage, compactEmbeddedAgentSessionDirect pour la compression), à 100% rétro-compatible. Des plugins tiers peuvent enregistrer leur propre engine via api.registerContextEngine("my-engine", factory), pour brancher politiques de résumé, retrieval augmentation, index vectoriels et autres features avancées.

Motivation de conception

Pourquoi abstraire le contexte en engine? Parce que plus l'agent tourne longtemps, plus le transcript s'allonge, plus le budget de tokens (token budget) est tendu. La stratégie native d'OpenClaw est « résumé par segments + préservation des messages importants + rotation des fichiers de session », figée dans compactEmbeddedAgentSessionDirect. Mais stratégies différentes selon modèle et scénario: Claude long-contexte privilégie « garder les N dernières + résumé au loin »; un scénario RAG-heavy veut « construire un index vectoriel en passant pendant le compact »; certains plugins veulent écrire dans la memory au moment du compact.

Si la stratégie de compression est figée dans l'agent runner, un plugin qui veut remplacer doit forker tout le runner. Une fois abstrait en interface ContextEngine, le plugin n'a qu'à implémenter ingest / assemble / compact / afterTurn, la boucle principale de l'agent se contente de les appeler au bon moment — la stratégie concrète est laissée à l'implémentation de l'engine.

Mais la pluginisation complète a aussi ses risques: un engine bugué peut directement perdre le contexte de conversation. C'est pourquoi le registry implémente aussi un mécanisme « quarantine » — si la factory de l'engine jette ou se comporte anormalement, l'id de l'engine est automatiquement isolé (quarantine), repli vers default, l'agent peut continuer.

Fichiers clés

Flux de données

Dans la boucle attempt, la compression est déclenchée par deux entrées: l'une proactively vérifie avant le prompt si les tokens approchent du budget, l'autre reactively compresse après que le provider a renvoyé une erreur overflow. Le chemin reactive (overflow compaction:2638) passe par compactContextEngineWithSafetyTimeout:

typescript
overflowCompactionAttempts++;
log.warn(
  `context overflow detected (attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS}); attempting auto-compaction for ${provider}/${modelId}`,
);
let compactResult: Awaited<ReturnType<typeof contextEngine.compact>>;
await runOwnsCompactionBeforeHook("overflow recovery");
try {
  const overflowCompactionRuntimeSettings = buildEmbeddedContextEngineRuntimeSettings(
    {
      tokenBudget: ctxInfo.tokens,
      degradedReason: "context_overflow",
    },
  );
  compactResult = await compactContextEngineWithSafetyTimeout(
    contextEngine,
    {
      sessionId: activeSessionId,
      sessionKey: params.sessionKey,
      sessionFile: activeSessionFile,
      tokenBudget: ctxInfo.tokens,
      ...(overflowTokenCountForCompaction !== undefined
        ? { currentTokenCount: overflowTokenCountForCompaction }
        : {}),
      force: true,
      compactionTarget: "budget",
      runtimeContext: overflowCompactionRuntimeContext,
      runtimeSettings: overflowCompactionRuntimeSettings,
    },
    resolveCompactionTimeoutMs(params.config),
    params.abortSignal,
  );

Quatre détails: force: true saute le check de threshold de l'engine, compresse sans discuter en cas d'overflow; compactionTarget: "budget" fait converger l'engine vers le token budget plutôt que vers un threshold; safety timeout vient de la config, un plugin bloqué est interrompu; abortSignal est au niveau run, si le run est annulé le compact doit s'arrêter aussi. Cet ensemble de contraintes est écrit explicitement dans le contrat compact() (compact contract:402):

typescript
  /**
   * Compact context to reduce token usage.
   * May create summaries, prune old turns, etc.
   *
   * The host always bounds this call with a finite safety timeout (the same
   * one that protects native runtime compaction). Engines that run long
   * operations SHOULD additionally honor `abortSignal` so an in-flight
   * compaction can be canceled promptly on run abort or host timeout instead
   * of running to completion in the background.
   */
  compact(params: {
    sessionId: string;
    sessionKey?: string;
    sessionFile: string;
    tokenBudget?: number;
    /** Force compaction even below the default trigger threshold. */
    force?: boolean;
    /** Optional live token estimate from the caller's active context. */
    currentTokenCount?: number;
    /** Controls convergence target; defaults to budget. */
    compactionTarget?: "budget" | "threshold";
    customInstructions?: string;
    /** Optional runtime-owned context for engines that need caller state. */
    runtimeSettings?: ContextEngineRuntimeSettings;
    runtimeContext?: ContextEngineRuntimeContext;
    /**
     * Optional abort signal honored before and during compaction. The host
     * aborts it on run-level abort or when its compaction safety timeout
     * fires; engines should stop work and reject promptly when it aborts.
     */
    abortSignal?: AbortSignal;
  }): Promise<CompactResult>;

Comment enregistre-t-on un engine? Le plugin appelle registerContextEngine via SDK (registerContextEngine:542), qui en interne passe à registerContextEngineForOwner:

typescript
export function registerContextEngineForOwner(
  id: string,
  factory: ContextEngineFactory,
  owner: string,
  opts?: RegisterContextEngineForOwnerOptions,
): ContextEngineRegistrationResult {
  const normalizedOwner = requireContextEngineOwner(owner);
  const registry = getContextEngineRegistryState().engines;
  const existing = registry.get(id);
  if (
    id === defaultSlotIdForKey("contextEngine") &&
    normalizedOwner !== CORE_CONTEXT_ENGINE_OWNER
  ) {
    // The default fallback id is core-owned; plugins can select other ids through slots.
    return { ok: false, existingOwner: CORE_CONTEXT_ENGINE_OWNER };
  }
  if (existing && existing.owner !== normalizedOwner) {
    return { ok: false, existingOwner: existing.owner };
  }
  if (existing && opts?.allowSameOwnerRefresh !== true) {
    return { ok: false, existingOwner: existing.owner };
  }
  registry.set(id, { factory, owner: normalizedOwner });
  clearContextEngineRuntimeQuarantine(id);
  return { ok: true };
}

Trois niveaux de protection: le default slot ne peut être enregistré que par core, un plugin ne peut pas le voler; si l'id est déjà pris par un autre owner, refus; same-owner refresh est off par défaut, pour éviter qu'un plugin écrase par erreur l'instance qu'il a déjà enregistrée. clearContextEngineRuntimeQuarantine nettoie les enregistrements de quarantine précédents en cas d'enregistrement réussi, permettant de réessayer un engine précédemment isolé.

Flux de resolution et repli quarantine:

Limites et modes d'échec

  • Verrouillage du default slot: l'id "legacy" est le default engine slot, seul CORE_CONTEXT_ENGINE_OWNER peut l'enregistrer, les plugins ne peuvent pas le voler — garantit que le repli est toujours atteignable.
  • Quarantine isolation: wrapContextEngineWithRuntimeQuarantine (src/context-engine/registry.ts:785) enveloppe chaque appel de méthode de l'engine d'un try-catch, en cas d'erreur écrit l'id de l'engine dans quarantinedEngines, les résolutions suivantes le skip directement. Les infos de quarantine sont persistées dans un stockage au niveau process, clearContextEngineRuntimeQuarantine nettoyé seulement au ré-enregistrement.
  • Safety timeout: compactContextEngineWithSafetyTimeout met un plafond dur via resolveCompactionTimeoutMs(config) sur le compact du plugin; en cas de timeout, jette, catch, traité comme déclencheur de quarantine. Le même timeout protège aussi la native runtime compaction, pour garantir comportement identique plugin vs native.
  • abortSignal doit être respecté: le contrat indique que l'engine « SHOULD » rejeter immédiatement sur abort, mais l'hôte ne dépend pas dur — car safety timeout sert de filet. Mais un engine qui ignore abort gaspille CPU/IO jusqu'au timeout.
  • Le compact de LegacyContextEngine est délégué: LegacyContextEngine.compact n'implémente pas d'algorithme lui-même, il appelle delegateCompactionToRuntime (src/context-engine/delegate.ts:34), qui lazy import compact.runtime.js pour appeler compactEmbeddedAgentSessionDirect. Ce pont permet à un engine tiers de réutiliser aussi le chemin de compression native — il suffit d'implémenter la différence sur assemble ou ingest, compact peut être délégué.
  • compactionTarget ignoré dans le chemin delegate: le commentaire de delegateCompactionToRuntime précise que le runtime native n'expose pas ce knob, donc le comportement compact de l'engine legacy n'est pas affecté par target. Un engine qui veut une compression target-specific doit implémenter compact lui-même.
  • Flag ownsCompaction: ContextEngineInfo.ownsCompaction (src/context-engine/types.ts:166-167) indique à l'hôte que l'engine gère lui-même le cycle de vie de compression, l'hôte ne déclenche plus automatiquement compact — pour les engines RAG-style complètement autonomes.
  • Repli default sur échec de resolution: id d'engine absent du registry, factory qui jette, ou engine retourné manquant une méthode, tous passent par resolveDefaultContextEngine (src/context-engine/registry.ts:1013), pour que la boucle agent ne tombe pas entièrement si un plugin n'est pas bien installé.
  • afterTurn déclenche la compaction proactive: compact() n'est pas seulement appelé sur overflow; le hook afterTurn (src/context-engine/types.ts:353-369) laisse l'engine décider après chaque tour s'il veut compacter; l'hôte lui passe le budget courant via tokenBudget, l'engine peut décider de compacter maintenant ou d'attendre le tour suivant.
  • Appelé aussi au spawn subagent: SubagentSpawnPreparation (src/context-engine/types.ts:182-187) laisse l'engine préparer un contexte isolé avant le démarrage du subagent, pour éviter la pollution croisée parent/enfant.

Résumé

Context Engine est l'abstraction pluggable du cycle de vie du contexte agent. Le LegacyContextEngine par défaut délègue tout aux chemins préexistants; les tiers injectent leur implémentation via registerContextEngine. Le registry protège le rayon d'échec des engines de plugin avec trois niveaux — owner check + verrouillage du default slot + quarantine; l'hôte contraint le temps compact via safety timeout + abortSignal. Comment le compact est déclenché concrètement par la boucle attempt dans boucle principale de l'agent; comment la memory injectée par l'engine est persistée sur disque dans fichiers de mémoire.

Pour comparer avec la documentation officielle: Context Engine docs · README.