我看完了。你的判断是对的,而且问题比“命名不顺眼”更深:context_policy.py 现在把 compaction 策略、事件 payload、归档 callback、summary 编码格式、tool-result 压缩都塞在一个类里,靠隐式约定串起来,可读性和可维护性都受影响。
主要问题
ArchiveEvent和events.ContextCompacted字段完全同构
lovia/context_policy.py 和 lovia/events.py 本质是同一个事件。现在 archive callback 用一个本地 event,runner 又重新构造ContextCompacted,会带来双份模型、双份文档、双份测试。entries_after可能不一致SummarizingContextPolicy._compact()archive 的entries_after是 policy 返回值;runner 的_on_context_compacted()会再_reinstate_system_prompt(),然后持久化的是被修改后的entries_log。但 emittedContextCompacted.entries_after仍然用原始entries_after,不是最终持久化版本。也就是说 archive、hook event、session 三者可能看到不同 transcript。summary marker 设计脆弱
_SUMMARY_PREFIX/_SUMMARY_SUFFIX和_SUMMARY_OPEN/_SUMMARY_CLOSE重复,extract_compaction_summary()用字符串解析 summary。runner 要知道SummarizingContextPolicy的私有文本格式,这破坏了 policy 抽象。只要用户自定义 policy 或 summary 内容碰巧含 marker,就会失效。ContextPolicy返回值太贫血
只返回list[TranscriptEntry],导致 runner 无法直接知道“为什么 compact、summary 是什么、是否 reactive、是否已归档”。于是只能用 identity check + marker parsing。这里应该返回结构化结果,而不是靠 list identity 传递语义。参数命名确实不直观
max_tokens实际是 context window cap,不是预算;keep_recent_messages实际保留的是TranscriptEntry数,不一定是用户理解的 message;reactive_keep_recent_messages又把“触发方式”和“保留窗口”耦合在名字里。compact_at_ratio也不如trigger_ratio或compact_when_prompt_exceeds清楚。usage.total_tokens不该直接用于 context window,但现在文档没讲清
context window 主要关心下一次 prompt 的 input tokens;total_tokens是累计 input+output 或单次总量,拿来判断 prompt 是否溢出会混淆“上下文窗口”和“费用/预算”。所以“不用 total_tokens”本身未必错,错在PolicyContext只叫last_prompt_tokens,而注释说得绕,容易让人误以为有完整 usage 信息却没用。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,用命名表达,不该靠长注释补救。“L2” 不可读
lovia/context_policy.py 的L2没有上下文,micro compaction也不是公共概念。建议改成trim_old_tool_results()/tool_result_retention,直接说“替换较旧 tool results”。circuit breaker 行为不够明确
proactive summarizer 失败会直接 raise,可能让一次普通 run 因后台压缩失败而失败;但 archive 失败又被吞掉。哪些失败是 best-effort、哪些失败要中断 run,需要一致设计。reactive 失败可以 raise,因为原本就 overflow;proactive 失败更适合 log 后返回原 transcript。默认 summarizer 复用主 provider 有风险
ProviderSummarizer(provider=None)会用当前模型总结长 transcript。方便,但可能贵、慢,而且 summarizer 的输入本身可能已经接近溢出。最好文档上强调,或者参数名显式为summary_provider。
推荐方案 A:最小兼容重构
保持 public API 基本不变,只修职责和命名:
用
ContextCompacted作为 archive callback payload,删除或弃用ArchiveEvent。把 summary marker 合并为单个 helper:
make_summary_entry(summary)/parse_summary_entry(entry)。extract_compaction_summary()只接受明确格式,例如content.startswith(SUMMARY_HEADER)。把
apply()中 token 计算拆为_prompt_tokens_for_current_entries().参数加新名字,旧名字保留 deprecation shim:
context_window_tokens替代max_tokenstrigger_ratio替代compact_at_ratiokeep_recent_entries替代keep_recent_messagesreactive_keep_recent_entries替代reactive_keep_recent_messageskeep_recent_tool_results改成tool_result_retentionrunner 发 event 时用最终持久化后的
entries_log,避免 event/session 不一致。
优点:改动小、兼容性好、测试容易迁移。
缺点:仍然保留 “policy 返回 list + runner 解析 summary” 这个根部设计。
推荐方案 B:结构化 CompactionResult
引入:
@dataclass
class ContextPolicyResult:
entries: list[TranscriptEntry]
summary: str | None = None
reason: Literal["threshold", "overflow", "tool_results"] | None = NoneContextPolicy.apply() 可以继续兼容旧的 list[TranscriptEntry] 返回,但新实现返回 ContextPolicyResult。runner 不再解析 marker,而是直接用 result.summary 生成 ContextCompacted。
优点:职责清楚,summary 不再靠字符串约定,未来支持更多 policy 更自然。
缺点:会碰 public protocol,需要兼容层和更多测试更新。
我最推荐的实现路线
先做方案 A 的兼容清理,再在一个小版本里引入方案 B。这样既能立刻解决重复事件、命名、注释、event/session 不一致等明显问题,又不会一次性破坏用户自定义 ContextPolicy。从 lovia 的“Concise / Backwards compatibility”哲学看,这条路线最稳。