Skip to content

E1|多前端:Agent 实现一次,前端只适配 ​

先别在 chatwidget 里实现智能 ​

A1 把六个入口钉在同一套本地 harness 上。读到 tui/src/chatwidget.rs 的人,下一步几乎都会把文本增量、审批弹窗、工具卡片当成「agent 逻辑」。然后 CI 里的 codex exec 再抄一份,IDE 再抄第三份。

判断先说清楚:智能在 Session 的 SQ/EQ 上(B1);前端只做三件事——提交 Op 的等价物、投影 Event、按自己的 I/O 契约说话。 TUI、codex exec、app-server、SDK 都嵌或连 同一份 app-server,而不是各自 ThreadManager::new。入口变的是审批 UX 和 stdout 纪律,不是 run_turn。

云端 chatgpt.com 仍是另一套 runtime(E3)。这一章只谈本仓库里的本地表面。


1. 插座是 app-server,不是 crate 里的 UI 模块 ​

TUI 启动会 start_embedded_app_server,得到 InProcessAppServerClient(tui/src/lib.rs:271-295)。还可以连本机 daemon 或远程 endpoint(:299-304)。embed / daemon / Remote 三种寿命见 G3。codex exec 同样 use InProcessAppServerClient(exec/src/lib.rs:21-24)。Python SDK 拉起 codex app-server --listen stdio://(A1)。IDE / 桌面是 JSON-RPC 客户端,UI 不在这棵树。

所以「多前端」不是四个 CodexThread 包装器,是 四种听 app-server 的方式:

前端怎么连人在不在回路
TUIin-process 或 daemon在;审批 overlay
codex execin-process默认不在
IDE / 桌面 / Python SDKstdio 或 socket 上的 JSON-RPC v2在或由宿主代批
TypeScript SDKspawn codex exec跟 CI 同构

内部 EventMsg 翻译成 ServerNotification 发生在 bespoke_event_handling.rs(B1)。前端禁止直接 reqwest 打模型、禁止自己 Command::new 跑 shell。AGENTS.md 禁止 library println!:TUI、app-server crate 都 deny(clippy::print_stdout, clippy::print_stderr)(tui/src/lib.rs:4-5,app-server/src/lib.rs:2)。用户可见输出必须走协议,才能同时服务终端、JSONL 和 IDE。

没有这只插座会怎样?每个表面复制一套工具列表和一份历史。协议一变四处一起炸——这正是 B1 总线存在的产品理由。


2. TUI:人在回路,审批是 overlay 不是核心 ​

无 subcommand 的 codex 进 TUI(A1)。它负责:把键盘变成 thread/start、turn/start、审批应答;把 notification 画成 transcript、diff、slash 菜单。

审批是 bottom pane overlay。用户验证弹窗写明自己是 presentation-only,不和通用审批状态机共用(tui/src/bottom_pane/user_verification.rs:1-4)。点 Allow / Deny 是对 app-server 的一次 RPC 回复,不是 TUI 自己改 SandboxPolicy。沙箱旋钮仍在 B3。

TUI 还可以连远程 app-server(手机看桌面 host,E3)。那时「前端」在这边,「Session」在 host 上。widget 仍然不能拥有智能。

E9 / E10 写 IDE 特有 UX 和 slash 索引。这里只要记住:TUI 厚在交互,薄在执行。


3. codex exec:同一条 turn,stdout 是契约 ​

exec crate 文件头把纪律写死(exec/src/lib.rs:1-6):

  • 默认模式:stdout 只能出现最终答案
  • --json:stdout 必须是 JSONL,一行一个事件
  • 其它一切进 stderr

无头默认 AskForApproval::Never(:563-568)。这是同意旋钮拧到不问(B3),不是关沙箱。CI 仍然可以是 workspace-write。需要人批就不要用这条路径,或显式改政策。

--ephemeral:不落盘 session(exec/src/cli.rs:35-37)。C5 的 Goal 挂不上。--ignore-user-config 仍用 CODEX_HOME 做鉴权(:39-40)。身份根和一次运行的配置覆盖不是一回事(B5)。

TypeScript SDK spawn 的就是这条管道(A1)。脚本、CI、一次性 turn 跟 codex exec 同构,不要再包一层「SDK 专用 agent」。

没有 stdout 契约会怎样?进度条和日志混进答案,管道对端解析失败;或 JSONL 里夹一行 TUI 色码。exec 禁 print_stdout(lib.rs:6)就是在强迫所有事件走 event_processor。


4. App Server v2:资源是 thread / turn,不是 chat message ​

协议在 app-server-protocol。方法名 <resource>/<method>,资源用单数:thread/start、turn/start(common.rs:559, 1033)。通知 thread/started、turn/started。这是给 IDE 和 SDK 的稳定插座;v1 不再加面积(仓库 app-server 开发规范)。

payload camelCase、可选字段在 *Params 上可空、不要 skip_serializing_if——那是给代码生成用的,不是给 TUI 看的。实验字段用 #[experimental]。

前端适配时:

  • 开会话 → thread/start(不是自己 ThreadManager)
  • 用户回车 → turn/start(带 input、审批、沙箱覆盖)
  • 听 ServerNotification 画 UI
  • 审批 → 对应的 response 方法,不要本地假放行

SessionSource 默认甚至是 VSCode(A1):IDE 从一开始就是一等客户端。Python SDK 的 thread_start / turn 就是这些 RPC 的语言皮。

没有 v2 资源模型会怎样?每个 IDE 发明一套 sendMessage。thread 级 Goal、fork、resume 无法共享语义。E1 的原则失效。


5. 两个 SDK 为什么连的不是同一个孔 ​

A1 写过:TS 走 codex exec,Python 走 app-server stdio。E1 补一句产品形状:

  • 管道(exec): 适合「跑完给我结果」。没有审批 overlay,stdout 即契约。
  • 插座(app-server): 适合「长生命周期宿主」。要订阅事件、要登录、要 Goal、要中途 steer。

不要做一个「统一 SDK」把两种 I/O 揉成 run(prompt) -> str。那会把 CI 和 IDE 逼回 call stack,B1 的队列就白设计了。


6. 前端允许做什么、禁止做什么 ​

允许: 投影 Event、做审批 UI、做 slash 到 RPC 的翻译、约束 stdout、把 --json 映射成自己的 schema(须标明是投影)。

禁止: 在 UI 里调模型;自己 exec 用户命令;自己写一份 memories/hooks 实现;把 Pets、主题、keymap 当核心(E8/E10);为了方便 println!。

桌面壳、IDE 扩展不在本仓库(A0)。它们若「看起来更智能」,查的是是不是连了另一版 app-server,而不是是不是另一套 run_turn。


7. 结语:带走一句话 ​

本地 Codex 的多前端是同一只 app-server 插座上的不同插头——TUI 做人在回路的 overlay,exec 守住 stdout,JSON-RPC v2 给 IDE 和 Python SDK;协议和 daemon 寿命见 G3;Agent 只实现一次,前端不许把智能写进 widget。