Skip to content

Context Engine: Abstraktion der Kontextkomprimierung

源码版本v2026.6.11

Verantwortung

Die Context Engine ist die Abstraktionsschicht für den Lebenszyklus des Agent-Kontexts (context): Wer Nachrichten ingestiert, wer den Prompt zusammenstellt, wer komprimiert (compact), wer das Transkript pflegt (maintain) — diese Entscheidungen sind aus der Agent-Hauptschleife herausgezogen und zu Methoden des ContextEngine-Interfaces geworden.

OpenClaw bringt eine LegacyContextEngine als Standardimplementierung mit, die alle Arbeiten an die bestehenden Pfade delegiert (SessionManager-Persistenz, attempt.ts-Assemblierung, compactEmbeddedAgentSessionDirect-Komprimierung) und 100 % Rückwärtskompatibilität wahrt. Drittanbieter-Plugins können über api.registerContextEngine("my-engine", factory) eine eigene engine registrieren und Strategien wie Zusammenfassung, Retrieval-Augmented Generation oder Vektorindizes anhängen.

Designmotivation

Warum den Kontext als engine abstrahieren? Weil je länger ein Agent läuft, desto länger das Transkript und desto knapper das Token-Budget (token budget). Die native Komprimierungsstrategie von OpenClaw lautet „segmentweise Zusammenfassung + wichtige Nachrichten behalten + Session-Datei rotieren" und ist in compactEmbeddedAgentSessionDirect fest verdrahtet. Verschiedene Modelle und Szenarien brauchen aber unterschiedliche Strategien: Claude Long Context behält am besten die letzten N Einträge + fernzusammenfassung; RAG-lastige Szenarien eignen sich, beim Komprimieren nebenbei einen Vektorindex aufzubauen; manches Plugin will beim Compact gleichzeitig memory schreiben.

Wäre die Komprimierungsstrategie fest im Agent-Runner verdrahtet, müsste ein Plugin zum Ersetzen den gesamten Runner forken. Als ContextEngine-Interface abstrahiert, implementiert ein Plugin nur ingest / assemble / compact / afterTurn; die Hauptschleife ruft sie zum richtigen Zeitpunkt auf — die konkrete Strategie überlässt es der engine.

Vollständige Plugin-isierung birgt aber Risiken: Ein engine-Bug verliert sofort den Konversationskontext. Deshalb implementiert die Registry einen Quarantäne-Mechanismus — wirft eine engine factory oder verhält sie sich anormal, wird diese engine-ID automatisch isoliert (quarantine) und auf default zurückgefallen, damit der Agent weiterläuft.

Schlüsseldateien

Datenfluss

Im attempt-Loop gibt es zwei Eintritte für die Komprimierung: proaktiv vor dem Prompt-Token-Budget prüfen, reaktiv nach einem Overflow-Fehler des Providers komprimieren. Der reaktive Pfad (overflow compaction:2638) läuft über 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,
  );

Vier Details: force: true überspringt die threshold-Selbstprüfung der engine — bei Overflow blind komprimieren; compactionTarget: "budget" lässt die engine auf das Token-Budget konvergieren statt auf threshold; das safety-Timeout kommt aus der config, Plugin-Hängen wird unterbrochen; abortSignal ist run-level — wird der run abgebrochen, muss auch compact stoppen. Diese Constraints sind im compact()-Vertrag klar (compact Vertrag: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>;

Wie wird eine engine registriert? Ein Plugin ruft über das SDK registerContextEngine (registerContextEngine:542), das intern an registerContextEngineForOwner weiterreicht:

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

Drei Schutzschichten: default-slot kann nur core registrieren, kein Plugin kann ihn stehlen; ist dieselbe ID bereits von anderem Owner registriert, wird abgelehnt; same-owner-refresh ist default aus, damit ein Plugin seine eigene Instanz nicht versehentlich überschreibt. clearContextEngineRuntimeQuarantine entfernt nach erfolgreicher Registrierung frühere quarantine-Einträge und erlaubt der engine, die einst isoliert war, erneute Versuche.

Der resolve-Fluss und der quarantine-Rückfall:

Grenzen und Fehler

  • default-slot gesperrt: Die ID "legacy" ist der default-engine-slot; nur CORE_CONTEXT_ENGINE_OWNER kann sie registrieren, kein Plugin kann sie stehlen — garantiert, dass der Fallback immer erreichbar bleibt.
  • Quarantäne-Isolation: wrapContextEngineWithRuntimeQuarantine (src/context-engine/registry.ts:785) wickelt jeden Methodenaufruf der engine in einen try-catch; bei Throw wird die engine-ID in quarantinedEngines geschrieben und bei künftigen resolve-Aufrufen übersprungen. Die quarantine-Info wird prozesslevel-persistent gespeichert; clearContextEngineRuntimeQuarantine wird nur bei Neuregistrierung aufgerufen.
  • Safety-Timeout: compactContextEngineWithSafetyTimeout gibt dem Plugin-compact über resolveCompactionTimeoutMs(config) eine harte Obergrenze; Timeout wirft und läuft in den catch, der als quarantine-Auslöser zählt. Dasselbe Timeout schützt auch die native runtime compaction; Plugin und native verhalten sich gleich.
  • abortSignal muss respektiert werden: Der Vertrag schreibt vor, dass die engine bei abort „SHOULD" sofort rejecten soll, aber der host hängt nicht zwingend daran — weil das safety-Timeout Rückfall ist. Eine engine, die abort ignoriert, verschwendet jedoch CPU/IO bis zum Timeout.
  • LegacyContextEngine.compact ist Delegation: LegacyContextEngine.compact implementiert keinen eigenen Algorithmus, sondern ruft direkt delegateCompactionToRuntime (src/context-engine/delegate.ts:34), das lazy compact.runtime.js importiert und compactEmbeddedAgentSessionDirect aufruft. Diese Brücke erlaubt auch Drittanbieter-engines, den nativen Komprimierungspfad wiederzuverwenden — nur assemble oder ingest mit abweichender Logik implementieren, compact delegieren.
  • compactionTarget im delegate-Pfad ignoriert: delegateCompactionToRuntime kommentiert explizit, dass die native runtime diesen Knob nicht exponiert, deshalb wird das compact-Verhalten der legacy engine nicht vom target beeinflusst. Engines mit target-spezifischer Komprimierung müssen compact selbst implementieren.
  • ownsCompaction-Marker: ContextEngineInfo.ownsCompaction (src/context-engine/types.ts:166-167) teilt dem host mit, dass diese engine den Komprimierungslebenszyklus selbst verwaltet; der host löst kein compact mehr automatisch aus — für vollständig autonome RAG-style-engines.
  • resolve-Fehlschlag fällt auf default zurück: engine-ID nicht in registry, factory wirft oder die engine fehlen Methoden — alle gehen über resolveDefaultContextEngine (src/context-engine/registry.ts:1013), damit der agent loop nicht komplett versagt, wenn ein Plugin nicht installiert ist.
  • afterTurn löst proaktive Komprimierung aus: compact() wird nicht nur bei Overflow gerufen; der afterTurn-Hook (src/context-engine/types.ts:353-369) lässt die engine nach jeder Runde selbst entscheiden, ob komprimiert wird. Der host übergibt über tokenBudget das aktuelle Budget; die engine kann jetzt oder nächste Runde komprimieren.
  • Subagent spawn ruft ebenfalls: SubagentSpawnPreparation (src/context-engine/types.ts:182-187) lässt die engine vor dem Start eines Subagents einen isolierten Kontext vorbereiten und verhindert Kontextverschmutzung zwischen Eltern- und Kind-Agent.

Zusammenfassung

Die Context Engine ist eine steckbare Abstraktion für den Lebenszyklus des Agent-Kontexts. Die Standard-LegacyContextEngine delegiert alles an die bestehenden Pfade; Drittanbieter injizieren über registerContextEngine eigene Implementierungen. Die Registry schützt das Fehler-Radius des Plugin-engines über drei Schichten (Owner-Prüfung + default-slot-Sperre + quarantine); der host bannt compact-Dauer über safety-Timeout + abortSignal doppelt. Wie genau compact durch den attempt-Loop ausgelöst wird, siehe Agent-Hauptschleife; wie der von der engine injizierte memory auf die Platte fällt, siehe Memory-Dateien.

Vergleich mit offiziellen Ressourcen: Context Engine-Doku · README.