Context Engine:コンテキスト圧縮抽象
責務
Context Engine は agent のコンテキスト (context) ライフサイクルの抽象層です:誰がメッセージを ingest するか、誰が prompt を組み立てるか、誰が圧縮 (compact) するか、誰が transcript を維持 (maintain) するか——これらの意思決定をすべて agent 主ループから引き離し,ContextEngine インターフェースのいくつかのメソッドに変換します。
OpenClaw は LegacyContextEngine をデフォルト実装として内蔵し,すべての作業を従来パス(SessionManager の永続化、attempt.ts の組み立て、compactEmbeddedAgentSessionDirect の圧縮)に委譲し,100% の後方互換性を保ちます。サードパーティプラグインは api.registerContextEngine("my-engine", factory) で自身の engine を登録し,要約ポリシー、検索拡張、ベクトル索引のような高度な機能を組み込めます。
設計動機
なぜ context を engine として抽象化するのか?agent が長く走るほど transcript は長くなり,トークン予算 (token budget) が厳しくなるからです。OpenClaw ネイティブの圧縮ポリシーは「分段要約 + 重要メッセージ保持 + session ファイルローテーション」で,このポリシーは compactEmbeddedAgentSessionDirect に硬結合されています。しかしモデルやシーンごとに必要な圧縮ポリシーは異なります:Claude の長コンテキストは直近 N 条 + 遠端要約の保持に向き,RAG-heavy なシーンは「圧縮ついでにベクトル索引を構築」に向き,一部のプラグインは compact 時に memory を同期的に書きたいこともあります。
圧縮ポリシーを agent runner に硬結合すると,プラグインが置き換えるには runner 全体を fork しなければなりません。ContextEngine インターフェースに抽象化すれば,プラグインは ingest / assemble / compact / afterTurn いくつかのメソッドを実装するだけでよく,agent 主ループは適切なタイミングでそれらを呼ぶだけです——具体ポリシーは engine 実装に任せます。
しかし完全なプラグイン化にもリスクがあります:engine にバグがあると会話コンテキストが直接失われます。だから registry は同時に「quarantine」機構を実装します——engine factory がエラーを投げたり異常挙動を示したりすれば,その engine id を自動隔離 (quarantine) し,default にフォールバック,agent が走り続けられるようにします。
主要ファイル
ContextEngine 接口:298-423—bootstrap/ingest/ingestBatch/assemble/compact/afterTurn/maintain/disposeの 8 メソッド。CompactResult:126-141— 圧縮結果契約,compacted: boolean+summary+firstKeptEntryId+tokensBefore/After+ ローテーション後のsessionId/sessionFile。compact() 契约:402-423—force/currentTokenCount/compactionTarget/customInstructions/abortSignalを含む。registry 核心:380-533—Symbol.for("openclaw.contextEngineRegistryState")グローバル singleton,モジュール間共有。registerContextEngineForOwner:508-533— 登録入口,owner 検証、default slot 保護、same-owner refresh 制御付き。registerContextEngine SDK:542-547— 公共 SDK 入口,PUBLIC_CONTEXT_ENGINE_OWNERでのみ登録でき,core id は奪えません。resolveContextEngine:906-950— slot で engine id を解決,失敗時は default にフォールバックし quarantine に打刻。wrapContextEngineWithRuntimeQuarantine:785-815— try-catch でラップ,engine メソッドがエラーを投げたら隔離 + フォールバック。ensureContextEnginesInitialized:16-24— 起動時にビルトイン legacy engine を登録,一度だけ実行。registerLegacyContextEngine:7-11—LegacyContextEngineを"legacy"id に登録,owner は"core"。LegacyContextEngine:22-88— デフォルト実装:ingestno-op、assemblepass-through、compactは runtime に委譲。delegateCompactionToRuntime:34-84— compact リクエストをcompactEmbeddedAgentSessionDirectに橋渡し。overflow compaction:2638-2741— attempt ループで溢出 (overflow) が圧縮をトリガーする主パス。compactContextEngineWithSafetyTimeout:170— プラグインの compact を有限 safety timeout で包み,ハングアップ防止。runCompactionPlanningWorker:56-180— 要約計画 worker,長い transcript をスライスして並行要約。
データフロー
attempt ループで圧縮をトリガーする入口は 2 つあります:一つは proactively,prompt 前に token が予算に接近しているかチェック;二つ目は reactively,provider が overflow エラーを返した後に圧縮。reactive パス(overflow compaction:2638)は 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,
);4 つの詳細: force: true は engine の threshold 自検をスキップし,溢出時は無条件で圧縮; compactionTarget: "budget" は engine に threshold ではなく token budget に収束させ; safety timeout は config から来て,プラグインがハングすると中断されます; abortSignal は run レベルで,run がキャンセルされると compact も停止。この制約は compact() 契約に明記されています(compact 契约: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>;engine はどう登録されるか?プラグインは SDK で registerContextEngine(registerContextEngine:542)を呼び,内部で 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 };
}3 層の保護: default slot は core しか登録できず,プラグインは奪えません; 同 id が別 owner で登録済みなら拒否; same-owner refresh はデフォルトでオフ,プラグインが誤って自身の登録済みインスタンスを上書きしないようにします。clearContextEngineRuntimeQuarantine は登録成功後に以前の quarantine 記録を消去し,隔離された engine の再試行を許可します。
resolve フローと quarantine フォールバック:
境界と失敗
- default slot ロック:
"legacy"という id は default engine slot で,CORE_CONTEXT_ENGINE_OWNERしか登録できず,プラグインは奪えません——fallback が常に到達可能であることを保証します。 - quarantine 隔離:
wrapContextEngineWithRuntimeQuarantine(src/context-engine/registry.ts:785)は engine のすべてのメソッド呼び出しを try-catch で包み,エラー時に engine id をquarantinedEnginesに書き込み,以降の resolve は直接スキップします。quarantine 情報はプロセスレベルのストレージに永続化され,clearContextEngineRuntimeQuarantineは再登録時にだけクリアします。 - safety timeout:
compactContextEngineWithSafetyTimeoutはresolveCompactionTimeoutMs(config)でプラグイン compact に硬い上限を与え,タイムアウト時はエラーを投げ catch に進み,quarantine トリガーとして扱われます。同じ timeout が native runtime compaction も保護し,プラグインと native の挙動一致を保証します。 - abortSignal は尊重されなければならない:契約で engine は「SHOULD」で abort 時に即座に reject すると明記されていますが,host は強く依存しません——safety timeout が最後の砦だからです。ただし abort を無視する engine は CPU/IO リソースをタイムアウトまで浪費します。
- LegacyContextEngine の compact は委譲:
LegacyContextEngine.compactは自前でアルゴリズムを実装せず,直接delegateCompactionToRuntime(src/context-engine/delegate.ts:34)を呼び,後者は lazy importcompact.runtime.jsしてcompactEmbeddedAgentSessionDirectを呼びます。この橋でサードパーティ engine も native 圧縮パスを再利用できます——assembleやingestの差分ロジックだけを実装し,compact は委譲できます。 compactionTargetは delegate パスで無視:delegateCompactionToRuntimeのコメント明記通り,native runtime はこの knob を公開しないため,legacy engine の compact 挙動は target の影響を受けません。target-specific な圧縮が必要な engine は自身で compact を実装しなければなりません。ownsCompactionマーク:ContextEngineInfo.ownsCompaction(src/context-engine/types.ts:166-167)は host にこの engine が自身で圧縮ライフサイクルを管理することを伝え,host は自動で compact をトリガーしません——完全自律の RAG-style engine 用です。- resolve 失敗時の default フォールバック:engine id が registry にない、factory がエラーを投げる、返された engine にメソッドがない,いずれも
resolveDefaultContextEngine(src/context-engine/registry.ts:1013)に進み,agent ループがプラグイン未インストールで完全に止まらないことを保証します。 - afterTurn が proactive compaction をトリガー:
compact()は overflow 時だけでなく,afterTurnフック(src/context-engine/types.ts:353-369)で engine は各ターン終了後に自発的に圧縮要否を判断でき,host はtokenBudgetパラメータで現在の予算を伝え,engine は今圧縮するか次ターンまで待つかを決められます。 - subagent spawn 時にも呼ばれる:
SubagentSpawnPreparation(src/context-engine/types.ts:182-187)で engine は subagent 起動前に隔離された context を準備し,親子 agent のコンテキスト汚染を回避します。
まとめ
Context Engine は agent コンテキストライフサイクルのプラグイン可能な抽象です。デフォルトの LegacyContextEngine はすべての作業を従来パスに委譲し,サードパーティは registerContextEngine で自身の実装を注入します。registry は owner 検証 + default slot ロック + quarantine の 3 層でプラグイン engine の失敗半径を保護し,host は safety timeout + abortSignal の二重制約で compact の長さを縛ります。具体的に compact が attempt ループでどうトリガーされるかは Agent 主ループ;engine に注入される memory がどうディスクに落ちるかは 記憶ファイル を参照。
公式資料:Context Engine 文档 · README。