Apply Patch 的流式预览不是对半截字符串做正则猜测。专用 StreamingPatchParser 增量识别已足够确定的 Hunk,转换成文件变化,并以 500ms 节流发送 PatchApplyUpdated;完成时再强制解析尾部和冲刷 pending 更新。
具体问题与启用条件
本节只研究模型仍在输出 patch 时的预览链。最终语法校验、审批和文件写入属于第六章 Apply Patch 专项。
| 条件来源 | 决定字段或状态 | 对本机制的影响 |
|---|---|---|
| 工具类型 | CustomToolCall 名称命中 apply_patch | Registry 提供专用 consumer |
| Feature | ApplyPatchStreamingEvents | 允许产生 PatchApplyUpdated |
| 流事件 | custom_tool_call_input.delta | 向 parser 推送新文本 |
协议、类型与状态所有权
| 状态或协议 | 所有者 | 生命周期 | 关键不变量 |
|---|---|---|---|
| StreamingPatchParser | ApplyPatchArgumentDiffConsumer | 一个 call | 跨 Delta 保存语法状态 |
| last_sent_at | consumer | 一个 call | 控制 500ms 节流窗口 |
| pending update | consumer | 到下一发送窗口或 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
机制怎样工作
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
失败、取消与恢复
| 故障或边界 | 已发生的状态 | 对上层的结果 | 能否直接重试 | 恢复动作 |
|---|---|---|---|---|
| Feature 关闭 | consumer 可存在但不解析 | 无预览 | 不适用 | 完整调用继续 |
| 半截语法尚不可判定 | parser 缓存输入 | 暂不发事件 | 等待更多 Delta | 下一段继续 push |
| 增量 parse 失败 | 不产生可靠 Hunk | 该次无预览 | 最终可修复 | 以 finish/完整调用为准 |
| finish 语法失败 | 文件仍未修改 | RespondToModel | 模型可修正 | 不进入执行 |
| 高频 Delta | pending 保存最新预览 | 中间帧被合并 | 不适用 | 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。
评论
登录后即可评论