对外协议是投影
IDE 看见的是 Thread / Turn / Item,不是内核 EventMsg。一次 turn/start 先回响应,再推事件流;审批是反向请求,不回包这一轮就停住。
本页解决的问题
先给结论「对外协议是投影」要解决的关键问题是什么?
IDE 看见的是 Thread / Turn / Item,不是内核 EventMsg。一次 turn/start 先回响应,再推事件流;审批是反向请求,不回包这一轮就停住。
跟着交接处走,不要只看 Demo。 系统是否可靠,往往取决于模型、工具、状态、权限和人的交接处。把每次交接都当成可以观察、测试和恢复的地方。
为一个自动化步骤写清输入、负责人、审批和恢复动作。
一次运行成功了,却说不清发生了什么,也无法安全重放。
turn/start 的回包只表示请求被接受,真正开跑看 turn/started;Python SDK 和 TypeScript SDK 走的不是同一条协议面。
turn/start 的 params。回车即播放。
turn/start 的响应立刻回来,只表示请求被接受。真正开始转圈,要等后面那条 turn/started 通知。你在给编辑器写插件。调试器里已经能看到内核往外抛事件:turn_started、exec_command_begin,字段是 snake_case。第一包数据过来,对不上。方法名是 turn/started,中间是斜杠。字段是 threadId、startedAt。
命令开始时你等的 exec_command_begin 没出现,来的是 item/started,里面塞着一个 type: "commandExecution" 的 item。审批更怪:服务端反向发来一条 request,你得回 response,否则这一轮卡在那儿。
如果编辑器按 81 种 EventMsg 写 switch,每加一种内部事件都是一次客户端升级。deprecated 别名也会从仓内兼容问题变成对外合同。
调度函数 apply_bespoke_event_handling 吃一条内核 Event,按四条规则收成对外消息。
EventMsg 的 snake_case type 变成 turn/started、item/agentMessage/delta 这种资源路径,字段改成 camelCase。
delta 和工具生命周期被收进 ThreadItem,再塞进 item/started 或 item/completed。IDE 按 item 的 type 画卡片。
ExecCommandBegin、ViewImageToolCall、以及 match 末尾的通配臂,线上没有对应通知。旧事件还在给 rollout 扇出。
一条 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 行
内核按发生了什么命名,对外按用户看见什么命名。内部还可以继续发 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:客户端问,等人回包。initialize、turn/start 是稳定面主干。
2. ServerNotification:服务端推,不等回包。turn/started、item/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 行
请求要回执,通知是广播,反向请求把人拉进环。这三件事混成一种,编辑器要么空转等开跑,要么漏画审批按钮。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,对外合同按投影层冻结。漏改投影,客户也看不见,只是功能丢了。
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 行回包到了,该不该转圈
turn/start 的响应已经回来,items 是空的。编辑器现在该转圈,还是该等 turn/started?如果内核新加一个 EventMsg 变体,投影没跟上,stdout 上会出现什么?
进阶一问:同一轮对话里模型要跑一条需要提问的命令。Python 客户端可以弹窗并回包,TypeScript 的 Thread.run() 为什么做不到?
「先玩一遍 · 一次请求怎么往返」的能力藏在每次交接里
「你在给编辑器写插件。调试器里已经能看到内核往外抛事件: turn_started 、 exec_command_begin ,字段是 snake_case。第一包数据过来,对不上。方法名是 turn/started ,中间是斜杠。字段是 threadId 、 startedAt」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。
先写清状态,再增加能力
从「命令开始时你等的 exec_command_begin 没出现,来的是 item/started ,里面塞着一个 type: "commandExecution" 的 item。审批更怪:服务端反向发来一条 request,你得回 response,否则这一轮卡在那儿」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。
成功路径不能代表系统可靠
用「进阶一问:同一轮对话里模型要跑一条需要提问的命令。Python 客户端可以弹窗并回包,TypeScript 的 Thread.run() 为什么做不到」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。
从「先玩一遍 · 一次请求怎么往返」走到「思路一 · 对外协议是投影」
「先玩一遍 · 一次请求怎么往返」先把问题落在「同一句话送进三种入口:看请求、事件流、响应怎么排 播放 单步 重置 入口 Python stdio TUI 内嵌 TS exec 用户输入 这句话会写进 turn/start 的 params。回车即播放。 当前阶段:还没发出请求。 请求 客户端发出,带 id 的等人回包 事件流 服务端推送,没有 id 响应 对得上请求 id 的回包 逻辑轨迹 · 动画每一步对应源码里的哪一段 点播放,看同一句话在三种入口里怎么走完…」上;到了「思路一 · 对外协议是投影」,讨论继续推进到「你在给编辑器写插件。调试器里已经能看到内核往外抛事件: turn_started 、 exec_command_begin ,字段是 snake_case。第一包数据过来,对不上。方法名是 turn/started ,中间是斜杠。字段是 threadId 、 startedAt」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。
把这条判断带到下一个场景
分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。
- 「先玩一遍 · 一次请求怎么往返」:同一句话送进三种入口:看请求、事件流、响应怎么排 播放 单步 重置 入口 Python stdio TUI 内嵌 TS exec 用户输入 这句话会写进 turn/start 的 params。回车即播放。 当前阶段:还没发出请求。 请求 客户端发出,带 id 的等人回包 事件流 服务端推送,没有 id 响应 对得上请求 id 的回包 逻辑轨迹 · 动画每一步对应源码里的哪一段 点播放,看同一句话在三种入口里怎么走完…
- 「思路一 · 对外协议是投影」:你在给编辑器写插件。调试器里已经能看到内核往外抛事件: turn_started 、 exec_command_begin ,字段是 snake_case。第一包数据过来,对不上。方法名是 turn/started ,中间是斜杠。字段是 threadId 、 startedAt
- 「最后的要点」:一条 ItemStarted(DynamicToolCall) 既发通知,又发 item/tool/call 这条 ServerRequest,等客户端执行
最后的「最后的要点」把讨论落到「一条 ItemStarted(DynamicToolCall) 既发通知,又发 item/tool/call 这条 ServerRequest,等客户端执行」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。
我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。
读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。
如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。
还没有这篇文章的讨论。