当前 Codex 并不存在一个为所有 Function Call 拼接 JSON 参数的通用流式累加器。标准 FunctionCall 的 arguments 在 OutputItemDone 中完整交付;只有 Custom Tool Input Delta 会路由给该工具可选的 Diff Consumer。
具体问题与启用条件
本节专门划清标准 Function、Custom Tool 与 Diff Consumer 的合同。apply_patch 如何利用该扩展点在 5.17 继续。
| 条件来源 | 决定字段或状态 | 对本机制的影响 |
|---|---|---|
| 线事件 | response.custom_tool_call_input.delta | 产生 ToolCallInputDelta |
| 活动项 | OutputItemAdded(CustomToolCall) | 按工具名创建可选 consumer |
| Handler | 实现 create_diff_consumer | 消费 partial input 并可发预览事件 |
协议、类型与状态所有权
| 状态或协议 | 所有者 | 生命周期 | 关键不变量 |
|---|---|---|---|
| 标准 FunctionCall.arguments | 完整 ResponseItem | Item Done 后 | JSON 字符串由 Router 后续解析 |
| active diff consumer | 采样事件循环 | Custom Item Added 到 Done | 最多一个并绑定 call_id |
| CustomToolCall.input | 完整 ResponseItem | Item Done 后 | 最终执行输入,不依赖 consumer 重建 |
机制调用链如下:
标准 Function:OutputItemAdded → 不创建 consumer → OutputItemDone(arguments 完整字符串)
Custom Tool:OutputItemAdded → ToolName → create_diff_consumer? → input delta* → call_id 过滤 → consumer events → Done 前 finish → 完整 CustomToolCall 执行
机制怎样工作
Wire Mapper 只识别 response.custom_tool_call_input.delta,从 item_id 或 call_id 建立事件身份。Core 看到 CustomToolCall Added 时,用 namespace + name 查询 Runtime Registry 的 Handler,并请求可选 ToolArgumentDiffConsumer;看到标准 FunctionCall Added 则明确把 Consumer 置空。
每个 ToolCallInputDelta 先与活动 call_id 比较:事件显式给出不同 ID 时直接跳过,没有 ID 时使用活动 ID。consumer 的 consume_diff 可返回客户端 EventMsg,但不负责构造最终工具调用。Item Done 前调用 finish(),清走 consumer;真正执行仍使用 Done Item 中完整 input/arguments。这保证预览解析失败或丢 Delta 不会改变工具执行事实。
Python 风格伪代码
这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。
async def on_tool_item_added(item: ResponseItem, registry: ToolRegistry):
if isinstance(item, CustomToolCall):
consumer = registry.create_diff_consumer(item.qualified_name)
return ActiveDiff(call_id=item.call_id, consumer=consumer)
if isinstance(item, FunctionCall):
return None # 标准函数参数等待 Done 完整交付
return None
async def on_tool_input_delta(active: ActiveDiff | None, event: ToolInputDelta):
if active is None:
return
if event.call_id is not None and event.call_id != active.call_id:
return
ui_event = active.consumer.consume_diff(event.delta) if active.consumer else None
if ui_event is not None:
await emit(ui_event)
async def on_tool_item_done(active: ActiveDiff | None, full_item: ResponseItem):
if active and active.consumer:
tail_event = active.consumer.finish()
if tail_event:
await emit(tail_event)
await route_complete_tool_call(full_item)
失败、取消与恢复
| 故障或边界 | 已发生的状态 | 对上层的结果 | 能否直接重试 | 恢复动作 |
|---|---|---|---|---|
| Delta call_id 不匹配 | 活动 consumer 保持 | 忽略该 Delta | 不适用 | 等待同 call_id 事件 |
| Handler 无 consumer | 完整调用仍会到达 | 无预览事件 | 不适用 | Done 后正常执行 |
| consumer partial parse 失败 | 预览状态可能不更新 | 通常不发事件 | 不影响执行 | 以完整 Done Item 为准 |
| finish 返回 RespondToModel | 完整调用尚未执行 | 工具错误路径 | 由模型修正 | 记录调用并反馈 |
设计取舍与验证
可选 consumer 避免给每种工具强加一套半成品 JSON 语义。最终执行不依赖 Delta,可抵抗丢帧和 parser 差异;代价是预览层与执行层会分别解析输入,必须接受预览可能保守或短暂落后。
| 可验证契约 | 证据方式 | 预期结果 |
|---|---|---|
| Custom input delta 保留 call_id | wire mapper 单元测试 | ToolCallInputDelta 字段正确 |
| 标准 Function 不建立 consumer | 事件循环分支审计 | 参数只在 Done 处理 |
| 不匹配 call_id 不污染活动预览 | consumer 路由测试 | 无客户端事件 |
Mini Codex 对照
Mini Codex 应把 ArgumentDiffConsumer 定义为可选 Protocol。标准函数先只支持 Done 时 json.loads(arguments);不要为了“看起来流式”而让执行依赖客户端拼接的半截 JSON。
本节边界
本节证明了 Diff Consumer 是 Custom Tool 的可选预览扩展,不是通用参数事实源。5.17 具体追踪 apply_patch 的 StreamingPatchParser 与 500ms 缓冲。
评论
登录后即可评论