Context Engine: abstraction de compression de contexte
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
ContextEngine interface:298-423— Huit méthodesbootstrap/ingest/ingestBatch/assemble/compact/afterTurn/maintain/dispose.CompactResult:126-141— Contrat de résultat de compression,compacted: boolean+summary+firstKeptEntryId+tokensBefore/After+sessionId/sessionFilepost-rotation.compact() contract:402-423— Inclutforce/currentTokenCount/compactionTarget/customInstructions/abortSignal.registry core:380-533— Singleton globalSymbol.for("openclaw.contextEngineRegistryState"), partagé cross-module.registerContextEngineForOwner:508-533— Entrée d'enregistrement, avec validation owner, protection du default slot, contrôle same-owner refresh.registerContextEngine SDK:542-547— Entrée SDK publique, ne peut enregistrer qu'avecPUBLIC_CONTEXT_ENGINE_OWNER, ne peut pas voler un id core.resolveContextEngine:906-950— Résout l'id d'engine par slot, repli vers default en cas d'échec et met en quarantine.wrapContextEngineWithRuntimeQuarantine:785-815— Enveloppe d'un try-catch, une méthode de l'engine qui jette est isolée + repli.ensureContextEnginesInitialized:16-24— Au démarrage, enregistre l'engine legacy builtin, ne s'exécute qu'une fois.registerLegacyContextEngine:7-11— EnregistreLegacyContextEngineà l'id"legacy", owner"core".LegacyContextEngine:22-88— Implémentation par défaut:ingestno-op,assemblepass-through,compactdélègue au runtime.delegateCompactionToRuntime:34-84— Ponte la requête compact verscompactEmbeddedAgentSessionDirect.overflow compaction:2638-2741— Chemin principal de compression déclenchée par overflow dans la boucle attempt.compactContextEngineWithSafetyTimeout:170— Enveloppe le compact du plugin avec un finite safety timeout, anti-blocage.runCompactionPlanningWorker:56-180— Worker de planning de résumé, slice un long transcript et résume en parallèle.
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:
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):
/**
* 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:
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, seulCORE_CONTEXT_ENGINE_OWNERpeut 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 dansquarantinedEngines, les résolutions suivantes le skip directement. Les infos de quarantine sont persistées dans un stockage au niveau process,clearContextEngineRuntimeQuarantinenettoyé seulement au ré-enregistrement. - Safety timeout:
compactContextEngineWithSafetyTimeoutmet un plafond dur viaresolveCompactionTimeoutMs(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.compactn'implémente pas d'algorithme lui-même, il appelledelegateCompactionToRuntime(src/context-engine/delegate.ts:34), qui lazy importcompact.runtime.jspour appelercompactEmbeddedAgentSessionDirect. Ce pont permet à un engine tiers de réutiliser aussi le chemin de compression native — il suffit d'implémenter la différence surassembleouingest, compact peut être délégué. compactionTargetignoré dans le chemin delegate: le commentaire dedelegateCompactionToRuntimepré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 hookafterTurn(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 viatokenBudget, 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.