OpenAI Codex · 对外协议

对外协议是投影

IDE 看见的是 Thread / Turn / Item,不是内核 EventMsg。一次 turn/start 先回响应,再推事件流;审批是反向请求,不回包这一轮就停住。

课程目标读完能说清三件事:对外协议是投影,内核事件会改名、丢掉或拆开之后才上线;turn/start 的回包只表示请求被接受,真正开跑看 turn/started;Python SDK 和 TypeScript SDK 走的不是同一条协议面。
先玩一遍 · 一次请求怎么往返
同一句话送进三种入口:看请求、事件流、响应怎么排
入口
这句话会写进 turn/start 的 params。回车即播放。
当前阶段:还没发出请求。
请求客户端发出,带 id 的等人回包
事件流服务端推送,没有 id
响应对得上请求 id 的回包
逻辑轨迹 · 动画每一步对应源码里的哪一段
    点播放,看同一句话在三种入口里怎么走完请求、事件流和响应。
    回包不是开跑turn/start 的响应立刻回来,只表示请求被接受。真正开始转圈,要等后面那条 turn/started 通知。
    审批是反向请求app-server 面上,命令审批以 ServerRequest 出现,客户端必须回包。TS exec 这条路上没有这套回函,人不在 JSONL 环里。
    载体可以换,合同尽量不换Python 走 stdio,TUI 走内存通道,消息形状仍是同一套斜杠加 camelCase。TS SDK 走的是另一条更窄的点号事件面。
    教学示意:消息条数与时机按协议形状编排,用于看清三路时序。逻辑轨迹右侧行号对应 openai/codex 仓库 commit 4f39251a01。
    思路一 · 对外协议是投影
    它解决什么问题

    你在给编辑器写插件。调试器里已经能看到内核往外抛事件:turn_startedexec_command_begin,字段是 snake_case。第一包数据过来,对不上。方法名是 turn/started,中间是斜杠。字段是 threadIdstartedAt

    命令开始时你等的 exec_command_begin 没出现,来的是 item/started,里面塞着一个 type: "commandExecution" 的 item。审批更怪:服务端反向发来一条 request,你得回 response,否则这一轮卡在那儿。

    如果编辑器按 81 种 EventMsg 写 switch,每加一种内部事件都是一次客户端升级。deprecated 别名也会从仓内兼容问题变成对外合同。

    思路是什么

    调度函数 apply_bespoke_event_handling 吃一条内核 Event,按四条规则收成对外消息。

    1. 改名

    EventMsg 的 snake_case type 变成 turn/starteditem/agentMessage/delta 这种资源路径,字段改成 camelCase。

    2. 换容器

    delta 和工具生命周期被收进 ThreadItem,再塞进 item/starteditem/completed。IDE 按 item 的 type 画卡片。

    3. 丢掉

    ExecCommandBeginViewImageToolCall、以及 match 末尾的通配臂,线上没有对应通知。旧事件还在给 rollout 扇出。

    4. 拆开

    一条 ItemStarted(DynamicToolCall) 既发通知,又发 item/tool/call 这条 ServerRequest,等客户端执行。

    出处:codex-rs/app-server/src/bespoke_event_handling.rs 第 159 至 188 行;codex-rs/app-server/src/bespoke_event_handling.rs 第 880 至 918 行;codex-rs/app-server/src/bespoke_event_handling.rs 第 996 至 1036 行

    item_event_to_server_notification 只覆盖一对一、无状态的投影。函数名像总入口,调用点才知道它是助手。ExecCommandBegin 在助手里还能变成 item/started,在调度里却走进 deprecated 空分支。现场命令卡片来自后面的 ItemStarted。以调度为准。

    出处:codex-rs/app-server-protocol/src/protocol/event_mapping.rs 第 25 至 37 行;codex-rs/app-server-protocol/src/protocol/item_builders.rs 第 1 至 11 行

    EventMsg 内核 81 个变体 调度函数 改名 / 丢掉 / 拆开 通配臂默认吞掉 通知 turn/started 通知 + 反向 request 丢掉,线上什么都没有 输入是内核事件,输出是 IDE 画卡片用的 Thread / Turn / Item
    教学化结构图:同一条 EventMsg,柜台决定留下、改名、丢掉还是拆成通知加回函。
    为什么长期成立

    内核按发生了什么命名,对外按用户看见什么命名。内部还可以继续发 deprecated 事件给 rollout,调度写一句注释丢掉即可。换语言重写,这张表还在:左边内部 type,右边写清留下、改名、丢掉还是拆开。

    未知行必须失败。空默认等于通配臂,新事件能通过编译,IDE 的 stdout 上什么都没有。

    出处:codex-rs/app-server/src/bespoke_event_handling.rs 第 1238 至 1245 行

    思路二 · 请求立刻回,事件随后到
    它解决什么问题

    同事把 turn/start 的响应当成一轮已经开始。响应立刻回来,里面是一份空 items 的 turn。模型还没开口。真正开跑是后面那条 turn/started 通知。

    出处:codex-rs/app-server/README.md 第 81 至 81 行

    审批做成普通 notification,客户端可以不理。turn 会停在等待上,直到超时或中断。

    思路是什么

    线上能解出来的对象只有四种:带 id 的 request、不带 id 的 notification、成功 response、错误 response。看起来像 JSON-RPC,结构体里没有 jsonrpc 字段。常量 JSONRPC_VERSION 还在,线上不带这个键。

    出处:codex-rs/app-server-protocol/src/rpc.rs 第 1 至 11 行;codex-rs/app-server-protocol/src/rpc.rs 第 34 至 72 行

    对外消息是四套,而且不对称。

    1. ClientRequest:客户端问,等人回包。initializeturn/start 是稳定面主干。

    2. ServerNotification:服务端推,不等回包。turn/starteditem/started 在这里。

    3. ServerRequest:服务端问人。第一条稳定方法是 item/commandExecution/requestApproval

    4. ClientNotification:展开之后只有 Initialized

    出处:codex-rs/app-server-protocol/src/protocol/common.rs 第 1663 至 1670 行;codex-rs/app-server-protocol/src/protocol/common.rs 第 1954 至 1956 行

    时间从左到右 turn/start 请求 立刻回 turn 对象 items 仍是空的 turn/started 通知 内核真的开始跑 随后是 item/started 与 delta 反向审批 request 客户端回包 通知没有 id。反向请求有 id,不回包这一轮就停住
    教学化时序图:回包、通知、反向请求是三件不同的事,落在三个不同的时刻。
    回包只表示请求被接受,开跑看通知。
    为什么长期成立

    请求要回执,通知是广播,反向请求把人拉进环。这三件事混成一种,编辑器要么空转等开跑,要么漏画审批按钮。id 对得上,过载时还能把 request 失败回给调用方,避免审批悬挂。

    思路三 · 实验面一次握手,进程内也不另造合同
    它解决什么问题

    实验方法有 57 个方法级标记。如果靠第二端口,稳定客户和冒险客户要连两个地方。TUI 如果因为同进程就改收 EventMsg,现场通知和远端 IDE 会各写一份 item。

    思路是什么

    实验面靠 initialize 时一个布尔 experimentalApi,缺省 false。再 initialize 会收到 Already initialized。没开开关就打 server/diagnostics,错误码 -32600,句子是固定的 server/diagnostics requires experimentalApi capability。Python SDK 把这个默认改成 True,官方脚本已经站在实验合同上。

    出处:codex-rs/app-server/src/message_processor.rs 第 891 至 895 行;sdk/python/src/openai_codex/client.py 第 209 至 209 行

    TUI 不直连 core。内嵌只换载体:socket 和 stdio 换成内存通道,MessageProcessor 还在。请求仍是 ClientRequest,响应仍走同一套 envelope。进程内是 transport-local,不是 protocol-free。

    出处:codex-rs/app-server/src/in_process.rs 第 1 至 24 行

    TypeScript SDK 不走这条路。它拼的是 exec --experimental-json,事件 type 是点号,字段是 snake_case,完整枚举只有 8 个变体。没有 initialize,没有审批 request。能力差在协议面,不差在语言。

    出处:sdk/typescript/src/exec.ts 第 89 至 90 行;codex-rs/exec/src/exec_events.rs 第 8 至 37 行

    为什么长期成立

    远程和本机的差别应落在网络,不落在语义。实验面用 capability,比文档里写一句实验更硬。一个布尔把稳定面和实验面切开,schema 生成出两份,默认那份不含实验字段。

    横向对比 · 共用类型,还是投影类型

    DeepSeek Harness:内核类型就是协议类型

    DSH 五个入口共用同一棵插件树。headless 的入口配置把自己写成 composition base:负责拼插件,不另写一套事件类型。跨进程时 Typert 从 TypeScript 类型图生成 stub,@Remote('create') 返回的是 identity,不是另一套展示模型。

    改一个事件字段,五张脸一起变。收益是不会出现 Python 看见 thread/started、TypeScript 看见 thread.started 这种分裂。Codex 反过来,内部可以标 deprecated 继续扇给 rollout,对外合同按投影层冻结。漏改投影,客户也看不见,只是功能丢了。

    两侧均已核对源码 · 2026-08-22 · examples/headless-agent/cordis.yml 第 1 至 4 行 · packages/goal/goal/src/index.ts 第 579 至 589 行 · DSH · 一个内核,五张面孔

    Claude Code:入口标记,没有第二协议面

    还原源码里能找到的是入口判断:CLAUDE_CODE_ENTRYPOINT === 'claude-vscode' 时返回 claude-vscode。没有对位的对外 IDE 协议 crate。扩展靠 MCP 和进程入口嵌进来,第三方 IDE 没有一份带 schema 的双向 RPC 可以对。

    Codex 付了投影层的维护成本,换来 VS Code 扩展、Python SDK 和本机 TUI 共用同一份 v2。

    已核对源码 · 2026-08-22 · restored-src/src/main.tsx 第 823 至 823 行
    课堂练习
    01

    回包到了,该不该转圈

    turn/start 的响应已经回来,items 是空的。编辑器现在该转圈,还是该等 turn/started?如果内核新加一个 EventMsg 变体,投影没跟上,stdout 上会出现什么?

    进阶一问:同一轮对话里模型要跑一条需要提问的命令。Python 客户端可以弹窗并回包,TypeScript 的 Thread.run() 为什么做不到?

    Takeaway:对外协议是一张投影表,不是内核枚举的 JSON 导出。请求立刻回,事件随后到,审批是反向请求。两个官方 SDK 走的不是同一条协议面。