本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
模块:T3 解剖 OpenAI Codex:把安全观写进类型系统
来源:xueai.miyang.cn(小山学堂 · 洛小山)
IDE 看见的是 Thread / Turn / Item,不是内核 EventMsg。一次 turn/start 先回响应、再推事件流;审批是反向请求,不回包这一轮就停住。
核心判断:对外协议是一张投影表,不是内核枚举的 JSON 导出。投影层的存在让内部可以自由演进(继续扇出 deprecated 事件给 rollout),同时把对外合同冻结在稳定面上。代价是必须维护这张表,且漏改不会报错,只会让客户端悄悄丢功能。
| 规则 | 含义 | 例子 |
|---|---|---|
| 改名 | snake_case 的 type → 资源路径 + camelCase 字段 | turn_started → turn/started,startedAt |
| 换容器 | delta 与工具生命周期收进 ThreadItem | 塞进 item/started / item/completed |
| 丢掉 | 线上没有对应通知 | ExecCommandBegin、ViewImageToolCall、通配臂 |
| 拆开 | 一条事件既发通知又发反向请求 | ItemStarted(DynamicToolCall) + item/tool/call |
出处:codex-rs/app-server/src/bespoke_event_handling.rs L159–188、L880–918、L996–1036。
| 类型 | 方向 | 是否等回包 | 稳定面内容 |
|---|---|---|---|
| ClientRequest | 客户端 → 服务端 | 等 | initialize、turn/start |
| ServerNotification | 服务端 → 客户端 | 不等 | turn/started、item/started |
| ServerRequest | 服务端 → 客户端 | 等(反向) | item/commandExecution/requestApproval |
| ClientNotification | 客户端 → 服务端 | 不等 | 展开后只有 Initialized |
出处:codex-rs/app-server-protocol/src/protocol/common.rs L1663–1670、L1954–1956;rpc.rs L1–11、L34–72。
| 入口 | 载体 | 协议面 |
|---|---|---|
| Python SDK | stdio | 完整 v2,含审批反向请求 |
| TUI(进程内) | 内存通道 | 同一套 v2,只换 transport |
| TypeScript SDK | exec --experimental-json | 更窄的点号事件面,8 个变体,无握手、无审批 |
你在给编辑器写插件。调试器里能看到内核往外抛事件(turn_started、exec_command_begin,snake_case)。第一包数据过来却对不上:方法名是 turn/started,字段是 threadId、startedAt。命令开始时你等 exec_command_begin,来的却是 item/started 里塞着一个 type: "commandExecution" 的 item。
如果编辑器按 81 种 EventMsg 写 switch,每加一种内部事件都是一次客户端升级,连 deprecated 别名都会从仓内兼容问题变成对外合同。
调度函数 apply_bespoke_event_handling 吃一条内核 Event,按上述四条规则收成对外消息。
item_event_to_server_notification 只覆盖一对一、无状态的投影。函数名像总入口,调用点才知道它是助手。
ExecCommandBegin 在助手里还能变成 item/started,在调度里却走进 deprecated 空分支。现场命令卡片来自后面的 ItemStarted。以调度为准。
出处:app-server-protocol/src/protocol/event_mapping.rs L25–37;item_builders.rs L1–11。
匹配里不能留空默认。空默认等于通配臂,新事件能通过编译,IDE 的 stdout 上什么都没有。
把漏改从「静默故障」推到「编译期报错」,是这一层最值钱的一条纪律。出处:bespoke_event_handling.rs L1238–1245。
四条规则摆在一起能看出一件事:投影不是格式转换。改名只是换了写法;换容器改变了聚合粒度;丢掉减少了对面积;拆开增加了交互次数。后三条都在改变客户端看到的世界形状,而不是把同一个东西换个编码 —— 这就是为什么不把它叫做序列化层。
客户端往往由第三方编写(编辑器扩展、内部工具、别人家的脚本),无法统一升级。若内核每加一种事件就要求所有客户端跟着改,成本会随客户端数量线性放大。有了一层投影,内部可自由重构,只要对外路径不变,外部代码一行都不用动。
相应的代价是:投影必须被持续维护,且漏改不会报错,只会让客户端悄悄丢功能。这正是「未知行必须失败」这条规则存在的理由。
同事把 turn/start 的响应当成一轮已经开始。响应立刻回来,里面是一份空 items 的 turn,模型还没开口。真正开跑是后面那条 turn/started 通知。出处:codex-rs/app-server/README.md L81。
若把审批做成普通 notification,客户端可以不理,turn 会停在等待上直到超时或中断。
线上能解出来的对象只有四种:带 id 的 request、不带 id 的 notification、成功 response、错误 response。看着像 JSON-RPC,结构体里没有 jsonrpc 字段,常量 JSONRPC_VERSION 还在但线上不带这个键。出处:rpc.rs L1–11、L34–72。
三件事落在三个不同时刻:
漏画审批按钮的后果比看起来严重:用户以为模型在思考,实际是卡在等授权。id 对得上时,过载还能把 request 失败回给调用方,避免审批悬挂。
experimentalApi,缺省 false。Already initialized。server/diagnostics,错误码 -32600,固定文案 server/diagnostics requires experimentalApi capability。出处:app-server/src/message_processor.rs L891–895;sdk/python/src/openai_codex/client.py L209。
TUI 不直连 core。内嵌只换载体:socket / stdio 换成内存通道,MessageProcessor 还在。请求仍是 ClientRequest,响应仍走同一套 envelope。出处:app-server/src/in_process.rs L1–24。
为什么长期成立:远程和本机的差别应落在网络,不落在语义。用 capability 切实验面比文档里写一句「实验」更硬 —— schema 生成出两份,默认那份不含实验字段。
TypeScript SDK 拼的是 exec --experimental-json,事件 type 是点号,字段 snake_case,完整枚举只有 8 个变体。没有 initialize,没有审批 request。出处:sdk/typescript/src/exec.ts L89–90;codex-rs/exec/src/exec_events.rs L8–37。
同一轮对话里模型要跑一条需要提问的命令:Python 客户端可以弹窗并回包;TypeScript 的 Thread.run() 做不到 —— 它压根不在有反向请求的环里。
能力差在协议面,不差在语言。 把这条当语言差异排查会浪费大量时间。
CLAUDE_CODE_ENTRYPOINT === 'claude-vscode'),没有对位的对外 IDE 协议 crate。第三方 IDE 靠 MCP 和进程入口嵌进来,没有带 schema 的双向 RPC 可对。客户端接入前逐条过:
item_event_to_server_notification 当总入口?turn/start 的响应当成「已开跑」?(应等 turn/started)quizFiles)一律不进入口播稿,仅作为集页下方的文字自测卡渲染。turn/start 的响应已经回来,items 是空的。编辑器现在该转圈,还是该等 turn/started?Thread.run() 为什么做不到?对外协议是一张投影表,不是内核枚举的 JSON 导出。请求立刻回,事件随后到,审批是反向请求。两个官方 SDK 走的不是同一条协议面。
*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*