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 走的不是同一條協議面。