Codewhale 输出展示过滤器解析:不污染规范会话记录的表示层压缩与筛选机制(RFC 4468)
本篇技术指南围绕 Codewhale 仓库中已接受的设计文档 docs/rfcs/4468-output-presentation-filters.md 展开:解释 Codewhale 为何拒绝在"模型响应与规范会话记录之间"运行任意用户脚本,而是将输出压缩/筛选收敛到一个显式的展示(presentation)与导出(export)层。读完本文,你将理解 show_thinking、codewhale exec --output-format stream-json 这两个"安全表面"的语义边界、它们背后的凭据完整性(receipt fidelity)约束,以及如何用结构化 JSON 行流在 Codewhale 外部自行实现有界、可审计的输出过滤管线。
背景:CIPHER 过滤器提案带来的需求与信任问题
在提交与评审 #4468 时,社区提出了一个称为 CIPHER 的输出压缩/过滤方向:希望在模型产生响应之后,用一个外部脚本来压缩或改写终端里呈现的内容,以服务可访问性(无障碍阅读)与自动化(harness 取数)需求。
RFC 承认这一需求是真实存在的——可访问性工具需要把冗长的思考块折叠成摘要,自动化 harness 需要只取 text 而跳过 thinking。但它同时指出:如果这类变换发生在持久化之前,被改写的内容就会进入规范会话记录(canonical session record),从而产生一系列无法挽回的后果。
RFC 给出的最终裁定非常明确(对应原文 Decision 一节):
Codewhale 不会在模型响应与规范会话记录之间运行任意用户脚本。模型原生的 assistant 与 thinking 块,始终是审计(audit)、回放(replay)、缓存计量(cache-accounting)、调试(debugging)以及 provider 签名(provider-signature)的唯一可信来源(source of truth)。
换句话说:模型输出的是"事实",任何压缩都是"派生视图",两者不允许混为一谈。 这一决策被接受进 v0.9.2 的产品边界。
核心设计:输出处理收敛到独立的表示/导出层
RFC 将"输出压缩"明确归类到一个独立的 presentation/export(展示/导出)层,第一版只支持既有安全表面,不引入任何新的任意命令执行能力:
| 表面 | 行为边界 |
|---|---|
TUI show_thinking = false |
只在渲染层隐藏 thinking,不删除消息/凭据路径里的规范内容 |
codewhale exec --output-format text|stream-json |
选择文档化的输出编码;结构化输出保留块身份,下游工具可自行挑选 thinking 或 text |
未来导出的 --view canonical|response-only|thinking-only 选择器 |
过滤后的导出必须自我标注为派生视图、携带规范会话引用,且绝不覆盖原会话 |
这一定位的关键词是 non-destructive(无破坏性)与 opt-in(显式选择):展示层可以决定"你看什么",但永远不改变"记录里有什么"。
为什么不能在持久化前改写:四项底层约束
凭据保真(Receipt fidelity)
会话本质是一份审计记录。如果内容在持久化前被替换:
- 无法证明 provider 实际发出了什么;
- 会破坏带签名的 Anthropic thinking 块(signed thinking blocks);
- 用量/费用凭据(usage/cost receipts)会与可见记录不一致。
因此 RFC 明确拒绝了两种存储策略:只存变换后形式(会导致无法审计)、默认同时存规范+变换双份(会双倍放大敏感数据、造成回放权威歧义)。在代码层,用量字段的权威性体现在 crates/protocol/src/event_msg.rs 的 TokenUsage 结构:reasoning_tokens、reasoning_replay_tokens、prompt_cache_hit_tokens 等字段按 provider 上报如实携带,None 表示"provider 未报告"而非零值——它们从不参考展示层有没有折叠过 thinking。
提示缓存与回放(Prompt cache and replay)
展示过滤器不会改变请求侧 token 用量。特别是 reasoning_replay_tokens 与 provider 专有的 signed-thinking 回放,必须使用规范形式。RFC 要求任何"压缩收益"的声明必须分开度量四类字节/token:
- 终端/导出字节(terminal/export bytes);
- 本地存储字节(local storage bytes);
- 请求侧回放 token(request-side replay tokens);
- provider 缓存命中行为(provider cache-hit behavior)。
v0.9.2 的边界只影响第 1 项。这也解释了为什么展示层的压缩不能宣称"省了推理 token"——那属于请求管线的事,与展示层无关。
流式语义(Streaming)
HookEvent::ResponseDelta 是纯观察者(observer-only)事件、以增量形式到达。若要做块级变换,就必须把 delta 缓冲到块结束,这会引入额外延迟并改变取消语义。在源码中,HookEvent::ResponseDelta 被定义为仅携带 response_id 与增量文本的观察事件(见 crates/hooks/src/lib.rs),它没有任何回写通道。
RFC 的结论是:展示消费者可以为自己的输出做缓冲,但引擎继续按规范形式增量发出并持久化。对应到实现,headless 执行路径在 crates/tui/src/core/engine/turn_loop.rs 明确要求对 exec / stream-json / app-server 的 stdout 保持 byte-clean,避免任何非 JSON 内容混入事件流。
信任与失败模式(Trust and failure)
由于没有新增任意命令执行,任何失败的过滤管线都不会破坏、延迟或替换会话。规范记录天然提供 fail-open 的兜底:即使外部消费者挂了,凭据依旧完好。外部消费者可以读取 stream-json 后,在 Codewhale 进程之外施加自己的有界变换。
第一个安全表面:show_thinking 只是显示开关
TUI 设置中的 show_thinking 是 RFC 列举的典型"展示层控制"。它只影响渲染出的会话抄本是否包含 thinking 内容,不影响规范消息/凭据路径。
在实现上,show_thinking 是 TUI 设置结构里的布尔字段,并接受 thinking 作为别名键(见 crates/tui/src/settings.rs 中 "show_thinking" | "thinking" => "show_thinking" 的归一化与解析逻辑)。配置文档 docs/CONFIGURATION.md 将其描述为 on/off 开关:开启 show_thinking 时 thinking 初始即展开显示,空格键仍可切换。可访问性说明 docs/ACCESSIBILITY.md 对它的定位描述得最贴切——设为 false 可从 TUI 呈现中隐藏模型的 reasoning_content 块,而 canonical session/replay receipts 保持不变。
值得注意的是,settings 实现中还刻意出现了一条注释级约束:"evidence-preserving —— show_thinking is deliberately left untouched"(见 crates/tui/src/settings.rs),即某些系统级路径不允许触碰该设置,从侧面印证了"它只是渲染偏好"这一设计意图。实践中建议把它当作纯显示偏好理解:需要彻底不看思考过程的场景用它,需要审计/复盘的场景直接看会话记录。
第二个安全表面:stream-json 结构化输出契约
stream-json 被 RFC 定义为可访问性与集成的正式接口,其契约要点如下:
- 事件是 JSON lines(NDJSON),一行一个 JSON 对象;
- response/thinking 的块身份保持显式;
- 工具/下游可以从自己的派生视图中省略某个块,但不得把该视图描述为规范会话;
- 不会为了过滤而附加环境变量映射、provider 凭据、隐藏工具载荷或无关抄本内容;
- 下游工具应自行限制输入、输出与处理时间。
在 CLI 帮助中,codewhale exec 对 --output-format <FORMAT> 的说明为 text 或 stream-json,并明确 stream-json 是自动化包装器使用的路径(见 crates/cli/src/lib.rs 中 exec 子命令的 after_help 示例:codewhale exec --auto --output-format stream-json "fix the failing test")。格式枚举定义在 crates/tui/src/lib.rs:ExecOutputFormat::{Text, StreamJson},其中 StreamJson 通过 #[value(name = "stream-json")] 绑定命令行取值。--json(汇总 JSON)与 stream-json 互斥,仓库里也有对应的回归测试(exec_json_conflicts_with_stream_json_output)。
实际运行时的事件形状(与 RFC 的"版本化"提醒)
RFC 原文给出了一条"response-only 呈现"的管道示例:
codewhale exec --output-format stream-json "..." \
| jq -r 'select(.type == "message_delta") | .text // empty'
同时它郑重提醒:确切事件名是版本化的运行时输出,调用方应当检查自己安装版本对应的 fixture,而不是从 RFC 推断字段。这条提醒在实际代码中得到了印证——本仓库 v0.9.2 边界的 exec 事件流实现于 crates/tui/src/exec_agent.rs 与 crates/tui/src/lib.rs,其中:
- 每条流事件对象都会被打上版本标记:
schema: "codewhale.exec-stream"与schema_version: 1(见exec_stream_value的实现); - 事件以
println!("{}", serde_json::to_string(...))逐行输出到 stdout(emit_exec_stream_event),天然符合"JSON lines 且 UTF-8 字节干净"的约束; - 实际事件
type包括content、tool_use、tool_result、agent_spawned、sandbox_denied、workflow_event、session_capture、turn_usage、metadata、done、error等(见ExecStreamEvent枚举)——模型正文增量(Event::MessageDelta)在StreamJson模式下会被映射为content事件(见 crates/tui/src/exec_agent.rs 的MessageDelta分支)。
因此,若要在当前版本实现 RFC 中"只取正文"的等价管道,应选择 type == "content" 的事件,例如:
codewhale exec --auto --output-format stream-json "explain this function" \
| jq -r 'select(.type == "content") | .content'
引擎级事件流与 TUI/CLI 的字节一致性
如果只想要"块身份显式"的细粒度事件,而不满足于 exec 聚合后的 content,仓库还提供了引擎级协议层:stream-json、app-server 的 SSE、以及 TUI 事件通道在协议层面讲的是同一种 EventMsg,使 headless 与 TUI 对同一个 Op 观察到字节一致的事件形状。文档级说明见 crates/protocol/src/event_msg.rs:
ResponseDelta增量事件带index与delta字段,并通过channel区分正文通道与推理通道(默认省略即text通道);kind_str统一为response_delta;MessageStarted / MessageComplete / ThinkingStarted / ThinkingComplete给出块的起止生命周期;- 引擎侧的
MessageDelta/ThinkingDelta到协议侧ResponseDelta(channel 分别为Text/Reasoning)的映射由 crates/tui/src/core/protocol_parity.rs 以编译期穷举匹配保证(新增引擎事件变体若没有协议孪生将无法通过编译)。
这条"协议奇偶校验"链路意味着:以 EventMsg 为格式的消费者拿到的是真正保留块身份的规范投影,用它做 thinking/text 的再选择是安全的。
无头 agent 的调用链与持久化语义
RFC 没有细讲但值得补充的是 stream-json 模式下的会话持久化行为。在 crates/tui/src/exec_agent.rs 中:
let should_persist_session = resuming_session || output_format == ExecOutputFormat::StreamJson;
即 stream-json 运行默认持久化会话(与文本模式不同),便于包装器在进程结束后仍可复现、续跑或审计该次运行。同时该文件明确注释:exec 的 stream-json 会有意省略推理增量(ThinkingDelta 分支被留空),thinking 仍保留在 TUI 抄本既有的 Activity Detail 表面中——这正是"展示层做减法、记录层保完整"的又一次体现。
另外,metadata 事件承载了可机读的分类结果:docs/MODES.md 中说明 exec --auto --output-format stream-json 每行输出一个 JSON 对象供 harness 与后端包装器消费,进程退出码 0 表示成功、1 表示真实任务/agent 失败、75(EX_TEMPFAIL)表示在会话内所有重试后仍因可重试的基础设施故障(provider/传输的 network/timeout)结束,终态 metadata 事件的 error_category 携带同样的分类。对应的映射函数 exec_failure_exit_code 位于 crates/tui/src/lib.rs,并特意将 rate_limit 排除在可重试码之外(配额耗尽不应被盲目重试打爆)。
被否决的备选方案:为什么它们走不通
RFC 逐条记录了被否决的路线,理解这些"不做什么"比理解"做什么"更能把握边界:
- 持久化前输出 hook(Pre-persistence output hook):会篡改审计/回放权威,直接违反凭据保真原则。
- 只改写 thinking(Mutate only thinking):带签名 thinking 与请求回放依然要求保真,改写即破坏。
- 默认同时存规范与变换双份(Store canonical plus transformed by default):重复的敏感数据 + 权威归属模糊,存储与合规成本不可接受。
- 提示模型自行缩写(Prompt the model to abbreviate):报告方实测采纳率为 0%,且它不是一个可靠的机械契约——不能把正确性押在模型的临场发挥上。
- 增设
[hooks.output_filter]配置表:在重复 hook 模式的同时,仍无法解决信任与凭据问题。
特别值得展开的是第 1、5 条背后的工程约束:当前 hook 系统(crates/hooks/src/lib.rs)中的流式事件本就是观察者模型——ResponseDelta 只携带 response_id 和 delta,hook stdout 从不具备响应改写权限。这正是 RFC 验收项"hook stdout 不得获得响应变更权威"的代码基础。任何试图把 stdout 解释为"改写指令"的设计都会破坏该约束,因此被整体排除。
未来的加法式扩展:可审计的派生导出 API
RFC 为后续演进保留了接口,而不是把当前拒绝变成永久死路。未来可能出现的 derived-export API 必须满足:
- 接受声明式、不可执行的选择器(declarative, non-executable selector);
- 写出包含 源会话 id(source session id)、源内容哈希(source content hash)、选择器(selector)、派生输出哈希(derived output hash) 的凭据;
- 任意可执行变换仍属于外部管线,除非后续安全评审定义好沙箱、披露(disclosure)、延迟与双形态保留(dual-form retention)语义。
可以看到,"哈希+选择器+引用"的设计意味着:即使未来允许派生导出,它产出的也始终是一份可被验证的派生视图,规范会话永远不会被覆盖。这与 v0.9.2 边界是一脉相承的。
验收清单与仓库证据的对照
RFC 的验收项可以在仓库中逐条找到证据:
| RFC 验收项 | 仓库证据 |
|---|---|
| 规范会话持久化与推理回放保持不变 | TokenUsage 含 reasoning_replay_tokens(crates/protocol/src/event_msg.rs);stream-json 模式显式持久化会话(crates/tui/src/exec_agent.rs) |
show_thinking 被文档化为仅显示层 |
设置字段+别名解析(crates/tui/src/settings.rs);docs/CONFIGURATION.md、docs/ACCESSIBILITY.md |
stream-json 被文档化为安全的机读过滤边界 |
CLI help 与示例(crates/cli/src/lib.rs);docs/MODES.md、docs/AGENT_RUNTIME.md |
| 无 hook stdout 获得响应变更权威 | HookEvent::ResponseDelta 仅为增量观察事件(crates/hooks/src/lib.rs) |
| 未引入新脚本/shell/凭据/网络能力 | exec 的事件枚举仅声明可序列化事件、无执行通道(crates/tui/src/lib.rs 的 ExecStreamEvent) |
实践结论
RFC 4468 的价值在于它把一个看似简单的需求("过滤输出")拆成了两个真正独立的问题:记录什么与呈现什么。Codewhale 的选择是前者永远由模型原生输出决定、只增不改;后者可以无限灵活,但必须显式地标记为派生视图并携带可回溯引用。
面向实践,你可以按这样的优先级使用本仓库能力:
- 只想在 TUI 里眼不见 thinking → 设置
show_thinking = false,零副作用; - 要接 CI/harness/另一个 agent,只取正文结果 →
codewhale exec --auto --output-format stream-json逐行消费,按type == "content"过滤(先检查你安装版本的流 fixture 确认事件名); - 要做有界的外部压缩/改写 → 在 Codewhale 之外对
stream-json施加自己的变换,自行限制输入/输出/耗时,并把变换产物当作派生视图而非会话; - 要完整保留 thinking/text 块身份的规范投影 → 对接引擎级
EventMsg/ResponseDelta通道(crates/protocol/src/event_msg.rs),由协议奇偶校验保证与 TUI 观测一致。
这样,可访问性工具的折叠诉求、自动化管线的取数诉求,与审计复核所需的凭据完整性,才能同时被满足——这正是 RFC 4468 被纳入 v0.9.2 产品边界的全部理由。RFC 全文与后续演进请继续参阅 docs/rfcs/4468-output-presentation-filters.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00