Context Engine: abstracción de compactación de contexto
Responsabilidad
Context Engine es el layer de abstracción del ciclo de vida del contexto (context) del agent: quién hace ingest de mensajes, quién ensambla el prompt, quién compacta (compact), quién mantiene (maintain) el transcript — estas decisiones se extraen del bucle principal del agent y se convierten en métodos de la interfaz ContextEngine.
OpenClaw trae un LegacyContextEngine como implementación por defecto que delega todo el trabajo de vuelta a las rutas existentes (SessionManager persiste, attempt.ts ensambla, compactEmbeddedAgentSessionDirect compacta), manteniendo 100% de compatibilidad hacia atrás. Los plugins de terceros pueden registrar su propio engine vía api.registerContextEngine("my-engine", factory), colgando estrategias avanzadas de resumen, retrieval augmentation o índices vectoriales.
Motivación de diseño
¿Por qué abstraer el contexto como engine? Porque cuanto más tiempo corre un agent, más largo es el transcript y más tenso el presupuesto de tokens (token budget). La estrategia nativa de compactación de OpenClaw es «resumen por segmentos + retención de mensajes importantes + rotación de archivos de sesión», codificada en compactEmbeddedAgentSessionDirect. Pero distintos modelos y escenarios necesitan distintas estrategias: Claude con contexto largo encaja con «mantener últimas N + resumen distante»; escenarios RAG-heavy prefieren «mientras compacta, construir índice vectorial»; algunos plugins quieren escribir memoria al compactar.
Si la estrategia se codifica en el agent runner, un plugin que quiera reemplazarla tendría que forkear todo el runner. Al abstraer en la interfaz ContextEngine, un plugin solo implementa ingest / assemble / compact / afterTurn y el bucle principal del agent los invoca en el momento adecuado — la estrategia concreta queda para el engine.
Pero la pluginización total también tiene riesgos: un bug en el engine podría perder el contexto de la conversación. Por eso el registry implementa también un mecanismo de «cuarentena» (quarantine): si la factory del engine lanza error o se comporta mal, se aísla automáticamente (quarantine) el id del engine, se cae al default y el agent puede seguir corriendo.
Archivos clave
ContextEngine interfaz:298-423— ocho métodos:bootstrap/ingest/ingestBatch/assemble/compact/afterTurn/maintain/dispose.CompactResult:126-141— contrato del resultado de compactación,compacted: boolean+summary+firstKeptEntryId+tokensBefore/After+sessionId/sessionFilerotados.compact() contrato:402-423— incluyeforce/currentTokenCount/compactionTarget/customInstructions/abortSignal.registry núcleo:380-533— singleton globalSymbol.for("openclaw.contextEngineRegistryState"), compartido entre módulos.registerContextEngineForOwner:508-533— entrada de registro, con validación de owner, protección del default slot, control de same-owner refresh.registerContextEngine SDK:542-547— entrada SDK pública, solo se puede registrar conPUBLIC_CONTEXT_ENGINE_OWNER, no puede robar ids core.resolveContextEngine:906-950— resuelve engine id por slot, falla al default y aplica quarantine.wrapContextEngineWithRuntimeQuarantine:785-815— envuelve con try-catch, si un método del engine lanza lo pone en cuarentena y cae al default.ensureContextEnginesInitialized:16-24— al arrancar registra el legacy engine builtin, se ejecuta solo una vez.registerLegacyContextEngine:7-11— registraLegacyContextEngineen el id"legacy", owner"core".LegacyContextEngine:22-88— implementación por defecto:ingestno-op,assemblepass-through,compactdelega al runtime.delegateCompactionToRuntime:34-84— puentea la petición de compact acompactEmbeddedAgentSessionDirect.overflow compaction:2638-2741— ruta principal de compactación por desbordamiento (overflow) en el bucle de attempt.compactContextEngineWithSafetyTimeout:170— envuelve el compact del plugin con un safety timeout finito, previene cuelgues.runCompactionPlanningWorker:56-180— worker de planificación de resumen, trocea transcripts largos y resume en paralelo.
Flujo de datos
En el bucle de attempt hay dos entradas para disparar la compactación: una proactiva, que comprueba antes de armar el prompt si los tokens están cerca del presupuesto, y otra reactiva, que compacta después de que el provider devuelva un error de overflow. La ruta reactiva (overflow compaction:2638) pasa por 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,
);Cuatro detalles: force: true salta el threshold check interno del engine, compacta sin discusión al desbordar; compactionTarget: "budget" hace que el engine converja al token budget en lugar de al threshold; el safety timeout viene de la config, un plugin colgado se interrumpe; abortSignal es a nivel run, si se cancela el run el compact también debe parar. Estas restricciones están claras en el contrato de compact() (compact contrato: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>;¿Cómo se registra un engine? El plugin invoca registerContextEngine desde el SDK (registerContextEngine:542), que internamente llama 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 };
}Tres capas de protección: el default slot solo lo registra core, un plugin no puede robarlo; si el id ya lo tiene otro owner, se rechaza; same-owner refresh está off por defecto, evitando que un plugin se pise a sí mismo. clearContextEngineRuntimeQuarantine limpia cualquier quarantine previa del id tras un registro exitoso, permitiendo reintentar un engine previamente aislado.
Flujo de resolve y fallback a quarantine:
Límites y fallos
- Default slot bloqueado: el id
"legacy"es el default engine slot, soloCORE_CONTEXT_ENGINE_OWNERpuede registrarlo, los plugins no pueden robarlo — garantiza que el fallback siempre esté disponible. - Quarantine de aislamiento:
wrapContextEngineWithRuntimeQuarantine(src/context-engine/registry.ts:785) envuelve cada invocación a métodos del engine con try-catch, y si lanza, escribe el id enquarantinedEnginespara que subsiguientes resolves lo salten. La info de quarantine se persiste a nivel proceso,clearContextEngineRuntimeQuarantinesolo se invoca al re-registrar. - Safety timeout:
compactContextEngineWithSafetyTimeoutusaresolveCompactionTimeoutMs(config)para poner un tope duro al compact del plugin, y si se excede lanza error, lo que se trata como disparador de quarantine. El mismo timeout también protege la compactación runtime nativa, garantizando comportamiento consistente entre plugin y nativo. - abortSignal debe respetarse: el contrato dice que el engine «SHOULD» rechazar de inmediato al abortar, pero el host no depende de ello — el safety timeout cubre. Un engine que ignore abort malgasta CPU/IO hasta que el timeout lo pare.
- El compact de LegacyContextEngine delega:
LegacyContextEngine.compactno implementa su propio algoritmo, llama directamente adelegateCompactionToRuntime(src/context-engine/delegate.ts:34), que hace lazy import decompact.runtime.jse invocacompactEmbeddedAgentSessionDirect. Este puente deja que un engine de terceros también reutilice la ruta de compactación nativa — solo necesita implementarassembleoingestdiferenciados y compact puede delegar. compactionTargetse ignora en la ruta delegate: el comentario dedelegateCompactionToRuntimedeja claro que el runtime nativo no expone este knob, así que el comportamiento del legacy engine no se ve afectado por target. Un engine que necesite compactación target-specific debe implementar compact por su cuenta.- Flag
ownsCompaction:ContextEngineInfo.ownsCompaction(src/context-engine/types.ts:166-167) le dice al host que este engine gestiona su propio ciclo de vida de compactación y el host ya no dispara compact automáticamente — pensado para engines RAG-style totalmente autónomos. - Resolve falla al default: si el engine id no está en el registry, la factory lanza, o el engine devuelto le faltan métodos, todo cae a
resolveDefaultContextEngine(src/context-engine/registry.ts:1013), garantizando que el bucle del agent no se caiga por completo si un plugin no se cargó. - afterTurn dispara compactación proactiva:
compact()no solo se invoca en overflow, el hookafterTurn(src/context-engine/types.ts:353-369) deja que el engine decida tras cada turno si compactar; el host le pasa eltokenBudgetactual y el engine decide si hacerlo ahora o esperar al siguiente turno. - Subagent spawn también invoca:
SubagentSpawnPreparation(src/context-engine/types.ts:182-187) deja que el engine prepare un contexto aislado antes de spawn un subagent, evitando que los contextos de agent padre e hijo se contamines.
Resumen
Context Engine es la abstracción plugable del ciclo de vida del contexto del agent. El LegacyContextEngine por defecto delega todo a las rutas existentes, y los terceros inyectan su implementación vía registerContextEngine. El registry usa tres capas — validación de owner + default slot bloqueado + quarantine — para contener el radio de fallo de un engine plugin, y el host usa safety timeout + abortSignal para acotar la duración del compact. Cómo dispara el bucle de attempt el compact se cubre en Bucle principal del agent; cómo la memoria inyectada por el engine cae a disco en Archivos de memoria.
Referencias oficiales: Documentación de Context Engine · README.