雨天小六

读懂 Codex(5.11):Previous Response ID 的保存与失效

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

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

response.created 出现 ID 并不代表它可作为下一请求的可靠父节点。Codex 只在 Completed 时把 response_id 与所有 Done Item 一起提交给 WebsocketSession;任何断流、重连或消费失败都会让引用链失效。

具体问题与启用条件

本节说明 ID 何时提交、一次性 receiver 怎样交接,以及服务端找不到 previous response 时如何回到完整请求。

条件来源决定字段或状态对本机制的影响
响应终态ResponseEvent::Completed提交 LastResponse
请求准备last_response_rx 可立即取到结果允许评估 previous response
服务端previous_response_not_found触发可重试并在下次发送完整请求

协议、类型与状态所有权

状态或协议所有者生命周期关键不变量
response_id + items_addedmap_response_events一次完成响应通过 oneshot 恰好提交一次
last_response_rxWebsocketSession到下一次 prepare被 take 后不可重复消费
last_response_from_untraced_warmupWebsocketSession到下一次请求决定 trace 记录完整逻辑请求

机制调用链如下:

OutputItemDone 累积 items_added
→ Completed 到达
→ oneshot.send(LastResponse)
→ WebsocketSession 保存 receiver
→ 下一请求 try_recv + take
→ 严格匹配后取 response_id
→ 发送 previous_response_id
→ 新响应 Completed 覆盖链头
Previous Response ID 从响应收集到一次性消费的状态机
图 5.11-1:Created 不进入 Ready;只有 Completed 同时提交 ID 与完整项。

机制怎样工作

Core 的 Created 事件不保存可复用 ID;真正的提交点在流映射器收到 Completed 时。它先记录 trace/usage,再通过 oneshot 发送 {response_id, items_added},随后把 Completed 转交 Runtime。这样 ID 与组成服务端上下文的完整项保持原子边界。

准备下一请求时 receiver 被 take()try_recv()。Empty、Closed、空 ID 都表示没有可靠父响应,当前请求发完整 Input。若 previous response 来自未进入 inference trace 的预热,线传输仍可用空 delta,但 trace 必须记录完整逻辑请求。服务端返回 previous_response_not_found 时,WS 适配器映射为 Retryable;本次 last_response receiver 不会产生 Completed,下一次尝试取不到父响应,自然退回完整请求。

Python 风格伪代码

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

async def map_stream(events, response_slot: FutureSlot):
    done_items: list[ResponseItem] = []
    async for event in events:
        if isinstance(event, OutputItemDone):
            done_items.append(event.item)
        if isinstance(event, Completed):
            response_slot.set_once(LastResponse(event.response_id, done_items.copy()))
        yield event

def take_previous(state: WsState) -> LastResponse | None:
    slot, state.last_response_slot = state.last_response_slot, None
    return slot.try_result() if slot is not None else None

async def send_with_previous(request: Request, state: WsState):
    previous = take_previous(state)
    incremental = compare_and_slice(request, state.last_request, previous)
    try:
        return await send(incremental or full(request))
    except PreviousResponseNotFound:
        state.last_response_slot = None
        raise RetryableError("retry full request")

失败、取消与恢复

服务端找不到 previous response 后的完整请求恢复
图 5.11-2:失败尝试不会产生新的 LastResponse,因此下一次准备自然关闭增量。
故障或边界已发生的状态对上层的结果能否直接重试恢复动作
流在 Completed 前断开可能已累积 Done Item不提交 LastResponse下一尝试完整请求
oneshot Closed/Empty没有可读父响应禁用本次增量不报致命错发送完整请求
response_id 为空Completed 数据不完整禁用增量不适用完整请求
previous_response_not_found服务端无父节点Retryable消耗旧 slot 后完整重发

设计取舍与验证

只在 Completed 提交会错过用 response.created 提前串联的性能机会,却避免引用一个失败或不完整响应。oneshot 精确表达“一次响应只被下一次准备消费一次”,但要求请求准备与流完成顺序正确;try_recv 的保守回退保证竞态不会产生错误增量。

可验证契约证据方式预期结果
Completed 才产生 LastResponse映射器事件序列测试半流 receiver 无值
预热 Response ID 支持空 delta首 Turn 请求捕获previous 有值且 input 为空
前缀失配废弃 previous修改历史后请求完整 input 且无 previous

Mini Codex 对照

Mini Codex 可用 asyncio.Future[LastResponse],但读取后必须清空槽位。不要在 Created 时写入 ID;如果服务端报告 previous 不存在,应把它归为一次可恢复的完整重发。

本节边界

ID 的提交和失效已经闭合。5.12 再向外看:何时整个 WebSocket 路径被 Session 级 HTTP 回退替代。

评论


← 返回文章列表