我看完了。你的判断是对的,而且问题比“命名不顺眼”更深:context_policy.py 现在把 compaction 策略、事件 payload、归档 callback、summary 编码格式、tool-result 压缩都塞在一个类里,靠隐式约定串起来,可读性和可维护性都受影响。

主要问题

  1. ArchiveEventevents.ContextCompacted 字段完全同构
    lovia/context_policy.pylovia/events.py 本质是同一个事件。现在 archive callback 用一个本地 event,runner 又重新构造 ContextCompacted,会带来双份模型、双份文档、双份测试。

  2. entries_after 可能不一致
    SummarizingContextPolicy._compact() archive 的 entries_after 是 policy 返回值;runner 的 _on_context_compacted() 会再 _reinstate_system_prompt(),然后持久化的是被修改后的 entries_log。但 emitted ContextCompacted.entries_after 仍然用原始 entries_after,不是最终持久化版本。也就是说 archive、hook event、session 三者可能看到不同 transcript。

  3. summary marker 设计脆弱
    _SUMMARY_PREFIX/_SUMMARY_SUFFIX_SUMMARY_OPEN/_SUMMARY_CLOSE 重复,extract_compaction_summary() 用字符串解析 summary。runner 要知道 SummarizingContextPolicy 的私有文本格式,这破坏了 policy 抽象。只要用户自定义 policy 或 summary 内容碰巧含 marker,就会失效。

  4. ContextPolicy 返回值太贫血
    只返回 list[TranscriptEntry],导致 runner 无法直接知道“为什么 compact、summary 是什么、是否 reactive、是否已归档”。于是只能用 identity check + marker parsing。这里应该返回结构化结果,而不是靠 list identity 传递语义。

  5. 参数命名确实不直观
    max_tokens 实际是 context window cap,不是预算;keep_recent_messages 实际保留的是 TranscriptEntry 数,不一定是用户理解的 message;reactive_keep_recent_messages 又把“触发方式”和“保留窗口”耦合在名字里。compact_at_ratio 也不如 trigger_ratiocompact_when_prompt_exceeds 清楚。

  6. usage.total_tokens 不该直接用于 context window,但现在文档没讲清
    context window 主要关心下一次 prompt 的 input tokens;total_tokens 是累计 input+output 或单次总量,拿来判断 prompt 是否溢出会混淆“上下文窗口”和“费用/预算”。所以“不用 total_tokens”本身未必错,错在 PolicyContext 只叫 last_prompt_tokens,而注释说得绕,容易让人误以为有完整 usage 信息却没用。

  7. apply() 那段大注释说明的是一个 bug workaround
    lovia/context_policy.py 的意思是:上一轮 provider 返回的 prompt token 统计已经过期,因为后来又追加了 assistant/tool/user entries,所以要用 max(previous_prompt_tokens, estimate(current_entries))。逻辑合理,但应该拆成一个 _current_prompt_tokens() helper,用命名表达,不该靠长注释补救。

  8. “L2” 不可读
    lovia/context_policy.pyL2 没有上下文,micro compaction 也不是公共概念。建议改成 trim_old_tool_results() / tool_result_retention,直接说“替换较旧 tool results”。

  9. circuit breaker 行为不够明确
    proactive summarizer 失败会直接 raise,可能让一次普通 run 因后台压缩失败而失败;但 archive 失败又被吞掉。哪些失败是 best-effort、哪些失败要中断 run,需要一致设计。reactive 失败可以 raise,因为原本就 overflow;proactive 失败更适合 log 后返回原 transcript。

  10. 默认 summarizer 复用主 provider 有风险
    ProviderSummarizer(provider=None) 会用当前模型总结长 transcript。方便,但可能贵、慢,而且 summarizer 的输入本身可能已经接近溢出。最好文档上强调,或者参数名显式为 summary_provider

推荐方案 A:最小兼容重构

保持 public API 基本不变,只修职责和命名:

优点:改动小、兼容性好、测试容易迁移。
缺点:仍然保留 “policy 返回 list + runner 解析 summary” 这个根部设计。

推荐方案 B:结构化 CompactionResult

引入:

@dataclass
class ContextPolicyResult:
    entries: list[TranscriptEntry]
    summary: str | None = None
    reason: Literal["threshold", "overflow", "tool_results"] | None = None

ContextPolicy.apply() 可以继续兼容旧的 list[TranscriptEntry] 返回,但新实现返回 ContextPolicyResult。runner 不再解析 marker,而是直接用 result.summary 生成 ContextCompacted

优点:职责清楚,summary 不再靠字符串约定,未来支持更多 policy 更自然。
缺点:会碰 public protocol,需要兼容层和更多测试更新。

我最推荐的实现路线

先做方案 A 的兼容清理,再在一个小版本里引入方案 B。这样既能立刻解决重复事件、命名、注释、event/session 不一致等明显问题,又不会一次性破坏用户自定义 ContextPolicy。从 lovia 的“Concise / Backwards compatibility”哲学看,这条路线最稳。