雨天小六

读懂 Codex(5.8):WebSocket 连接状态机和请求相关性

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

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

一条 WebSocket 能承载多次 response.create,但 Codex 没有靠任意 request_id 在客户端并发复用。每个响应期间独占连接流,直到 Completed 才允许下一请求,因此相关性由串行所有权而非猜测事件归属保证。

具体问题与启用条件

本节解释连接泵、独占锁、帧解析和请求相关性。连接预热与跨 Turn 转移在 5.9,previous response 增量在 5.10—5.11。

条件来源决定字段或状态对本机制的影响
Providersupports_websockets=true允许连接 /responses 升级
Sessiondisable_websockets=false仍优先选择 WS
连接未关闭且无活动响应可复用同一物理流

协议、类型与状态所有权

状态或协议所有者生命周期关键不变量
WsStream pumpResponsesWebsocketConnection物理连接串行处理 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 才能发送
Responses WebSocket 的连接和活动响应状态机
图 5.8-1:只有 Completed 返回可复用 Connected;其余终止都回到 Disconnected。

机制怎样工作

底层 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

失败、取消与恢复

两个逻辑请求在同一 WebSocket 上串行相关
图 5.8-2:B 必须等待 A 的 Completed 和锁释放,事件不会交错。
故障或边界已发生的状态对上层的结果能否直接重试恢复动作
发送超时/失败请求可能未完整到达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 基线。

评论


← 返回文章列表