雨天小六

读懂 Codex(5.17):Apply Patch 参数 Diff 怎样产生预览事件

· 更新于 2026-08-02 · 专栏:读懂 Codex

#Codex#Agent Runtime#Responses API#流式协议#软件架构

Apply Patch 的流式预览不是对半截字符串做正则猜测。专用 StreamingPatchParser 增量识别已足够确定的 Hunk,转换成文件变化,并以 500ms 节流发送 PatchApplyUpdated;完成时再强制解析尾部和冲刷 pending 更新。

具体问题与启用条件

本节只研究模型仍在输出 patch 时的预览链。最终语法校验、审批和文件写入属于第六章 Apply Patch 专项。

条件来源决定字段或状态对本机制的影响
工具类型CustomToolCall 名称命中 apply_patchRegistry 提供专用 consumer
FeatureApplyPatchStreamingEvents允许产生 PatchApplyUpdated
流事件custom_tool_call_input.delta向 parser 推送新文本

协议、类型与状态所有权

状态或协议所有者生命周期关键不变量
StreamingPatchParserApplyPatchArgumentDiffConsumer一个 call跨 Delta 保存语法状态
last_sent_atconsumer一个 call控制 500ms 节流窗口
pending updateconsumer到下一发送窗口或 finish只保留最新完整预览
PatchApplyUpdatedEvent协议事件客户端暂态按 call_id 关联,不代表文件已写

机制调用链如下:

CustomToolCall Added
→ 创建 ApplyPatch consumer
→ 每个 delta: parser.push_delta
→ 得到当前可确认 hunks
→ 转换为 Path→FileChange
→ 500ms gate
→ PatchApplyUpdated
→ Item Done: parser.finish
→ flush pending latest preview
Apply Patch 从参数 Delta 到节流预览的时序
图 5.17-1:解析器先确认语法,节流器再决定立即发送或合并为最终快照。

机制怎样工作

Feature 未启用时 consume_diff 立即返回 None,但完整 apply_patch 调用照常执行。启用后,parser 只有在新增文本形成可识别 Hunk 时才返回变化;语法尚不完整或解析错误不会发猜测事件。Hunk 被转换为协议层 FileChange,包含 Add/Update/Delete 等路径变化。

第一次有效变化立即发送。距离上次发送不足 500ms 时,不把所有中间更新排队,而是用最新 event 覆盖 pending;超过间隔的下一次有效变化直接发送并清 pending。Item Done 前 finish() 要求 parser 能完成整段语法;成功后发送 pending 的最后快照,失败转成 RespondToModel。预览事件不触碰文件系统,也不替代后续审批与正式解析。

Python 风格伪代码

这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。

class ApplyPatchPreviewConsumer:
    INTERVAL = 0.5

    def __init__(self):
        self.parser = StreamingPatchParser()
        self.last_sent: float | None = None
        self.pending: PatchPreview | None = None

    def consume_diff(self, call_id: str, delta: str, now: float) -> PatchPreview | None:
        hunks = self.parser.push(delta)  # 不完整时返回空,不猜测
        if not hunks:
            return None
        event = PatchPreview(call_id, convert_hunks(hunks))
        if self.last_sent is not None and now - self.last_sent < self.INTERVAL:
            self.pending = event  # coalesce 为最新状态
            return None
        self.last_sent, self.pending = now, None
        return event

    def finish(self) -> PatchPreview | None:
        try:
            self.parser.finish()
        except PatchSyntaxError as error:
            raise RespondToModel(f"failed to parse apply_patch: {error}")
        event, self.pending = self.pending, None
        return event

失败、取消与恢复

Patch 预览的语法、节流和失败边界
图 5.17-2:解析失败停在预览/模型反馈层,不会越过执行与审批边界。
故障或边界已发生的状态对上层的结果能否直接重试恢复动作
Feature 关闭consumer 可存在但不解析无预览不适用完整调用继续
半截语法尚不可判定parser 缓存输入暂不发事件等待更多 Delta下一段继续 push
增量 parse 失败不产生可靠 Hunk该次无预览最终可修复以 finish/完整调用为准
finish 语法失败文件仍未修改RespondToModel模型可修正不进入执行
高频 Deltapending 保存最新预览中间帧被合并不适用finish 冲刷最后一帧

设计取舍与验证

500ms coalescing 限制大 patch 的 UI 和事件压力,代价是预览非逐字符实时。保留最新快照而非所有中间 diff 符合“预览当前状态”的用途。专用 streaming parser 增加实现成本,却比通用 JSON/string 累加更能保证文件路径和 Hunk 语义。

可验证契约证据方式预期结果
首个完整 Hunk 立即发预览分段 push 测试Add File 先以空内容出现
500ms 内更新合并操纵 last_sent_at无中间事件、pending 更新
finish 冲刷完整文件内容完成 patch 后 finish最后预览含所有行

Mini Codex 对照

Mini Codex 可以复用正式 patch parser 的增量前端;若只会字符串累计,应干脆不发预览,不能把未经语法确认的路径展示为即将修改。预览事件必须明确标为 tentative。

本节边界

这里的结果只是客户端预览。第六章 6.32—6.40 将继续说明 Grammar、路径验证、审批、实际写入和 TurnDiffTracker。

评论


← 返回文章列表