Skip to content

Context Engine: abstracción de compactación de contexto

源码版本v2026.6.11

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

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:

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

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):

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

¿Cómo se registra un engine? El plugin invoca registerContextEngine desde el SDK (registerContextEngine:542), que internamente llama 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 };
}

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, solo CORE_CONTEXT_ENGINE_OWNER puede 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 en quarantinedEngines para que subsiguientes resolves lo salten. La info de quarantine se persiste a nivel proceso, clearContextEngineRuntimeQuarantine solo se invoca al re-registrar.
  • Safety timeout: compactContextEngineWithSafetyTimeout usa resolveCompactionTimeoutMs(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.compact no implementa su propio algoritmo, llama directamente a delegateCompactionToRuntime (src/context-engine/delegate.ts:34), que hace lazy import de compact.runtime.js e invoca compactEmbeddedAgentSessionDirect. Este puente deja que un engine de terceros también reutilice la ruta de compactación nativa — solo necesita implementar assemble o ingest diferenciados y compact puede delegar.
  • compactionTarget se ignora en la ruta delegate: el comentario de delegateCompactionToRuntime deja 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 hook afterTurn (src/context-engine/types.ts:353-369) deja que el engine decida tras cada turno si compactar; el host le pasa el tokenBudget actual 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.