Skip to content

Context Engine:上下文压缩抽象

源码版本v2026.6.11

职责

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 能继续跑。

关键文件

数据流

attempt 循环里,触发压缩有两个入口:一是 proactively 在 prompt 前检查 token 是否接近预算,二是 reactively 在 provider 返回 overflow 错误后再压。reactive 路径(overflow compaction:2638)走的是 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,
  );

四个细节: force: true 跳过 engine 的 threshold 自检,溢出时无脑压; compactionTarget: "budget" 让 engine 收敛到 token budget 而不是 threshold; safety timeout 来自 config,plugin 卡死会被中断; abortSignal 是 run-level 的,run 被取消时 compact 也得停。这套约束在 compact() 契约里写得很清楚(compact 契约: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>;

engine 怎么注册?plugin 通过 SDK 调 registerContextEngine(registerContextEngine:542),它内部转给 registerContextEngineForOwner:

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

三层保护: 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:compactContextEngineWithSafetyTimeoutresolveCompactionTimeoutMs(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 import compact.runtime.jscompactEmbeddedAgentSessionDirect。这条桥让第三方 engine 也能复用 native 压缩路径——只需要实现 assembleingest 的差异化逻辑,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