Context Engine: Abstraktion der Kontextkomprimierung
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
ContextEngine-Interface:298-423— Acht Methoden:bootstrap/ingest/ingestBatch/assemble/compact/afterTurn/maintain/dispose.CompactResult:126-141— Komprimierungsergebnis-Vertrag:compacted: boolean+summary+firstKeptEntryId+tokensBefore/After+ rotiertesessionId/sessionFile.compact() Vertrag:402-423— Enthältforce/currentTokenCount/compactionTarget/customInstructions/abortSignal.registry Kern:380-533—Symbol.for("openclaw.contextEngineRegistryState")globales Singleton, modulübergreifend geteilt.registerContextEngineForOwner:508-533— Registrierungs-Eintritt mit Owner-Prüfung, default-slot-Schutz und same-owner-refresh-Kontrolle.registerContextEngine SDK:542-547— Öffentlicher SDK-Eintritt; nur mitPUBLIC_CONTEXT_ENGINE_OWNERregistrierbar; darf keine core-ID stehlen.resolveContextEngine:906-950— Löst die engine-ID nach slot auf; bei Fehlschlag Rückfall auf default und quarantine.wrapContextEngineWithRuntimeQuarantine:785-815— Wickelt einen try-catch; wirft eine engine-Methode, wird sie isoliert + zurückgefallen.ensureContextEnginesInitialized:16-24— Registriert beim Start die eingebaute legacy-engine; läuft nur einmal.registerLegacyContextEngine:7-11— RegistriertLegacyContextEngineunter der ID"legacy"mit owner"core".LegacyContextEngine:22-88— Standardimplementierung:ingestno-op,assemblepass-through,compactdelegiert an die Runtime.delegateCompactionToRuntime:34-84— Brückt compact-Anfragen ancompactEmbeddedAgentSessionDirect.overflow compaction:2638-2741— Hauptpfad der durch Overflow ausgelösten Komprimierung im attempt-Loop.compactContextEngineWithSafetyTimeout:170— Wickelt das compact des Plugins in ein endliches Safety-Timeout gegen Hängen.runCompactionPlanningWorker:56-180— Zusammenfassungsplanungs-Worker, der lange Transkripte slice-parallel zusammenfasst.
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:
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):
/**
* 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:
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; nurCORE_CONTEXT_ENGINE_OWNERkann 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 inquarantinedEnginesgeschrieben und bei künftigen resolve-Aufrufen übersprungen. Die quarantine-Info wird prozesslevel-persistent gespeichert;clearContextEngineRuntimeQuarantinewird nur bei Neuregistrierung aufgerufen. - Safety-Timeout:
compactContextEngineWithSafetyTimeoutgibt dem Plugin-compact überresolveCompactionTimeoutMs(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.compactimplementiert keinen eigenen Algorithmus, sondern ruft direktdelegateCompactionToRuntime(src/context-engine/delegate.ts:34), das lazycompact.runtime.jsimportiert undcompactEmbeddedAgentSessionDirectaufruft. Diese Brücke erlaubt auch Drittanbieter-engines, den nativen Komprimierungspfad wiederzuverwenden — nurassembleoderingestmit abweichender Logik implementieren, compact delegieren. compactionTargetim delegate-Pfad ignoriert:delegateCompactionToRuntimekommentiert 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; derafterTurn-Hook (src/context-engine/types.ts:353-369) lässt die engine nach jeder Runde selbst entscheiden, ob komprimiert wird. Der host übergibt übertokenBudgetdas 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.