Context Engine:上下文壓縮抽象
職責
Context Engine 是 agent 上下文 (context) 生命週期的抽象層:誰負責 ingest 訊息、誰負責組裝 prompt、誰負責壓縮 (compact)、誰負責維護 (maintain) transcript——這些決策都從 agent 主循環裡抽出來,變成 ContextEngine 介面的幾個方法。
OpenClaw 內建一個 LegacyContextEngine 作為預設實作,它把所有工作委託回原有路徑(SessionManager 持久化、attempt.ts 組裝、compactEmbeddedAgentSessionDirect 壓縮),保持 100% 向後相容。第三方外掛可以透過 api.registerContextEngine("my-engine", factory) 註冊自己的 engine,把摘要策略、檢索增強、向量索引這些高階特性掛進來。
設計動機
為什麼把 context 這塊抽象成 engine?因為 agent 跑得越久,transcript 越長,token 預算 (token budget) 越緊張。OpenClaw 原生的壓縮策略是「分段摘要 + 保留重要訊息 + 旋轉 session 檔案」,這套策略寫死在 compactEmbeddedAgentSessionDirect 裡。但不同模型、不同場景需要的壓縮策略不同:Claude 長上下文適合保留最近 N 條 + 遠端摘要;RAG-heavy 場景適合「壓縮時順便建立向量索引」;有些 plugin 想在 compact 時同步寫 memory。
如果把壓縮策略寫死在 agent runner 裡,plugin 想替換就得 fork 整個 runner。抽成 ContextEngine 介面後,plugin 只要實作 ingest / assemble / compact / afterTurn 幾個方法,agent 主循環只負責在合適的時機調它們——具體策略留給 engine 實作。
但完全外掛化也有風險:engine 出 bug 會直接把對話上下文搞丟。所以 registry 同時實作了「quarantine」機制——engine factory 拋錯或行為異常,自動隔離 (quarantine) 這個 engine id,回退到 default,讓 agent 能繼續跑。
關鍵檔案
ContextEngine 介面:298-423—bootstrap/ingest/ingestBatch/assemble/compact/afterTurn/maintain/dispose八個方法。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")全域單例,跨模組共享。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— 用 finite safety timeout 包裹 plugin 的 compact,防卡死。runCompactionPlanningWorker:56-180— 摘要規劃 worker,把長 transcript 切片並行摘要。
資料流
attempt 循環裡,觸發壓縮有兩個入口:一是 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,
);四個細節: force: true 跳過 engine 的 threshold 自檢,溢出時無腦壓; compactionTarget: "budget" 讓 engine 收斂到 token budget 而不是 threshold; safety timeout 來自 config,plugin 卡死會被中斷; abortSignal 是 run-level 的,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 怎麼註冊?plugin 透過 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 };
}三層保護: default slot 只能 core 註冊,plugin 搶不走; 同 id 已被其他 owner 註冊時拒絕; same-owner refresh 預設關,避免 plugin 誤覆蓋自己已註冊的實例。clearContextEngineRuntimeQuarantine 在註冊成功後清掉之前的 quarantine 記錄,允許重新嘗試被隔離過的 engine。
resolve 流程和 quarantine 回退:
邊界與失敗
- default slot 鎖定:
"legacy"這個 id 是 default engine slot,只有CORE_CONTEXT_ENGINE_OWNER能註冊,plugin 不能搶——保證 fallback 永遠可達。 - quarantine 隔離:
wrapContextEngineWithRuntimeQuarantine(src/context-engine/registry.ts:785)把 engine 每次方法呼叫包一層 try-catch,拋錯時把 engine id 寫進quarantinedEngines,後續 resolve 直接跳過它。quarantine 資訊持久化到進程級儲存,clearContextEngineRuntimeQuarantine只在重新註冊時清。 - safety timeout:
compactContextEngineWithSafetyTimeout用resolveCompactionTimeoutMs(config)給 plugin compact 加硬上限,逾時拋錯走 catch,被當 quarantine 觸發。同一個 timeout 也保護 native runtime compaction,保證 plugin 和 native 行為一致。 - abortSignal 必須被尊重:契約裡寫明 engine「SHOULD」在 abort 時立刻 reject,但 host 不強依賴——因為 safety timeout 兜底。但忽略 abort 的 engine 會浪費 CPU/IO 資源到 timeout 才停。
- 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 loop 不會因為 plugin 沒裝好就完全跑不起來。 - 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 三層保護 plugin engine 的失敗半徑,host 用 safety timeout + abortSignal 雙重約束 compact 時長。具體 compact 怎麼被 attempt 循環觸發看 Agent 主循環;engine 注入的 memory 怎麼落到磁碟看 記憶檔案。
對照官方資料:Context Engine 文件 · README。