一条 WebSocket 能承载多次 response.create,但 Codex 没有靠任意 request_id 在客户端并发复用。每个响应期间独占连接流,直到 Completed 才允许下一请求,因此相关性由串行所有权而非猜测事件归属保证。
具体问题与启用条件
本节解释连接泵、独占锁、帧解析和请求相关性。连接预热与跨 Turn 转移在 5.9,previous response 增量在 5.10—5.11。
| 条件来源 | 决定字段或状态 | 对本机制的影响 |
|---|---|---|
| Provider | supports_websockets=true | 允许连接 /responses 升级 |
| Session | disable_websockets=false | 仍优先选择 WS |
| 连接 | 未关闭且无活动响应 | 可复用同一物理流 |
协议、类型与状态所有权
| 状态或协议 | 所有者 | 生命周期 | 关键不变量 |
|---|---|---|---|
| WsStream pump | ResponsesWebsocketConnection | 物理连接 | 串行处理 send 与 inbound frame |
Mutex<Option<WsStream>> | 连接对象 | 物理连接 | 一次响应任务持锁到终态 |
| ResponseStream queue | 一次 response.create | 逻辑响应 | 只接收独占期内的帧 |
| turn_state/treatment | 响应读取任务 | Turn/流 | metadata 可更新本地解析策略 |
机制调用链如下:
GET /responses WebSocket upgrade
→ WsStream pump 接管 socket
→ stream_request 序列化 response.create
→ 响应任务锁住 WsStream
→ send frame
→ 逐帧解析到 ResponseEvent
→ Completed 释放锁
→ 下一 response.create 才能发送
机制怎样工作
底层 pump 在一个 select 中同时接收发送命令和网络帧;Ping 直接回 Pong,Text/Close 等帧转入内部通道。stream_request 为一次逻辑响应建立 1600 容量队列,并启动任务。这个任务获取 Mutex<Option<WsStream>> 后一直持有 guard,直到 run_websocket_response_stream 完成。
因此当前实现并不允许两个 Responses 请求在同一 socket 上交错。Text frame 不需要携带客户端自建 correlation id;它天然属于持锁任务。response.created 只是生命周期事件,可靠的 previous response ID 要等 response.completed 交付。出现 Binary、Close、EOF 或 idle timeout 都会将连接从 Option 中 take 并丢弃,防止下一请求复用已失步的帧流。
Python 风格伪代码
这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。
class ResponsesWebSocket:
def __init__(self, socket: WebSocket):
self.socket = socket
self.response_lock = asyncio.Lock()
self.closed = False
async def stream_request(self, request: dict) -> AsyncIterator[ResponseEvent]:
async with self.response_lock: # 相关性边界
await asyncio.wait_for(self.socket.send_json(request), self.idle_timeout)
while True:
frame = await asyncio.wait_for(self.socket.receive(), self.idle_timeout)
if frame.kind == "close":
self.closed = True
raise StreamError("closed before completed")
if frame.kind != "text":
raise StreamError("unexpected websocket frame")
event = map_wire_event(frame.text)
if event is not None:
yield event
if isinstance(event, Completed):
return
失败、取消与恢复
| 故障或边界 | 已发生的状态 | 对上层的结果 | 能否直接重试 | 恢复动作 |
|---|---|---|---|---|
| 发送超时/失败 | 请求可能未完整到达 | Stream 错误 | 是 | 废弃连接后重连 |
| Unexpected Binary | 连接协议失步 | Stream 错误 | 是 | take 并丢弃 socket |
| Close/EOF 无 Completed | 可能已有部分事件 | Stream 错误 | 是 | 不复用连接 |
| 消费者提前丢弃 | 读取任务失去接收方 | 记录取消 | 不在该流继续 | 外层 Turn 决定 |
设计取舍与验证
串行独占牺牲了一条连接承载并发 Responses 的吞吐,却让相关性、取消和连接恢复大幅简单。若改为 request_id 多路复用,必须再维护每请求队列、乱序终态、背压隔离和断连时的批量失败。Coding Agent 单 Turn 的采样本就按工具结果串行推进,当前取舍与上层控制流一致。
| 可验证契约 | 证据方式 | 预期结果 |
|---|---|---|
| 请求 payload 保持 Responses 字段 | 序列化单元测试 | 只额外加入 type/previous/generate |
| Ping/Pong 不进入业务流 | 连接泵测试 | 业务队列只见内容帧 |
| Close 前无 Completed 报错 | 模拟 Close frame | 连接不可再复用 |
Mini Codex 对照
Mini Codex 应用一个 asyncio.Lock 包围“发送到 Completed”的整个范围。即使 WebSocket 库允许并发 send,也不要在未实现 correlation router 前并发 response.create。
本节边界
现在一条连接可以安全串行承载请求。5.9 说明启动阶段怎样提前准备这条连接和首个 previous-response 基线。
评论
登录后即可评论