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。