Codex App Server:Agent协议为什么选双向JSON-RPC
把一个 Agent 嵌进 IDE、桌面应用或者 Web,不是简单暴露一个 /chat 接口就够了。
用户一句“跑测试并修掉失败项”,背后可能产生:
用户输入
模型增量输出
Shell 执行
文件 Diff
审批请求
测试结果
新的模型回合
最终回答
OpenAI 最近公开 Codex App Server 的设计时,给了一个非常值得借鉴的答案:Codex CLI、IDE Extension、Web 和 Desktop App 底层复用同一套 Harness,通过一个双向 JSON-RPC 风格协议把 Agent Loop 暴露给客户端。
真正有价值的不是“用了 JSON-RPC”,而是它把 Agent 交互拆成了三个稳定原语:
Thread
Turn
Item
再用事件生命周期把流式进度、Tool、Diff 和 Approval 统一起来。
这比把 Agent 当成一个长连接聊天接口更接近生产系统。
为什么普通 HTTP request/response 很快就不够用
传统接口:
POST /chat
请求:
{
"message": "run tests and summarize failures"
}
响应:
{
"answer": "..."
}
对于 Agent,这个模型丢掉了大量信息。
测试运行 40 秒期间,客户端需要知道:
当前正在执行什么?
有没有文件修改?
是否需要用户批准?
任务能否取消?
断线后怎么恢复?
所以一个用户请求对应的不再是一个响应,而是:
1 Request
→ N Events
→ 可能再由 Server 发起 Request
这也是为什么协议必须双向。
OpenAI 的三个核心原语
Thread
Thread 是一条长期 Agent 会话。
它可以:
create
resume
fork
archive
并持久化事件历史。
这解决的是:
浏览器关闭
IDE 重启
网络断开
之后还能恢复同一任务上下文。
Turn
Turn 是一次用户驱动的工作单元。
例如:
“运行测试并总结失败。”
从输入开始,到 Agent 完成这一轮工作结束。
一个 Thread 可以包含多个 Turn。
Item
Item 是最小原子单元。
类型可以是:
User Message
Agent Message
Tool Execution
Approval Request
Diff
Item 有明确生命周期:
item/started
item/*/delta
item/completed
这个模型特别适合 UI。
为什么 started → delta → completed 很实用
例如 Shell Tool:
{
"method": "item/started",
"params": {
"item_id": "tool-91",
"type": "shell_execution",
"command": "mvn test"
}
}
执行过程中:
{
"method": "item/shell/delta",
"params": {
"item_id": "tool-91",
"chunk": "Tests run: 128..."
}
}
结束:
{
"method": "item/completed",
"params": {
"item_id": "tool-91",
"exit_code": 1,
"duration_ms": 18232
}
}
客户端不需要猜:
这条日志是不是已经结束?
状态由协议明确表达。
这和 SSE 有什么区别
SSE 很适合:
Server → Client
持续推送事件。
但 Agent 还需要:
Server → Client:需要 Approval
Client → Server:Approve / Deny
双向 JSON-RPC 更自然。
在 Web 场景,OpenAI 的做法是:浏览器到 Codex Backend 使用 HTTP + SSE;Worker 内部启动 App Server,并通过长期 JSON-RPC/stdin-stdout 通道与 Harness 通信。
所以协议层可以分成:
Browser Transport
和
Agent Harness Protocol
不必强行使用同一种技术。
Approval 为什么必须由 Server 主动发请求
假设 Agent 想执行:
rm -rf build/generated
或者修改文件。
Server 可以主动发:
{
"id": "approval-12",
"method": "approval/request",
"params": {
"action": "shell",
"command": "...",
"risk": "MEDIUM"
}
}
客户端返回:
{
"id": "approval-12",
"result": {
"approved": true
}
}
Agent Turn 在此期间暂停。
这和“模型输出一句请确认,然后下一轮希望用户回答 yes”差别很大。
前者是协议级状态。
后者只是聊天约定。
我会把 Approval 做成一等协议对象
public record ApprovalRequest(
String approvalId,
String threadId,
String turnId,
String itemId,
ActionType actionType,
String actionHash,
RiskLevel risk,
Instant expiresAt) {
}
Response:
public record ApprovalResponse(
String approvalId,
boolean approved,
String decidedBy,
Instant decidedAt) {
}
执行时再次验证 actionHash。
防止批准后参数被替换。
initialize Handshake 不是多余仪式
Codex App Server 要求客户端先发一次 initialize。
这让双方可以协商:
Protocol Version
Capabilities
Feature Flags
Defaults
例如:
{
"id": 1,
"method": "initialize",
"params": {
"client": "my-ide",
"protocol_version": "2",
"capabilities": {
"diff_stream": true,
"approval_ui": true
}
}
}
Server:
{
"id": 1,
"result": {
"protocol_version": "2",
"server_features": {
"thread_fork": true,
"skills": true
}
}
}
这对长期兼容非常关键。
Agent Protocol 最难的是兼容,不是能传 JSON
客户端发布周期不同:
VS Code Extension
JetBrains
Xcode
Desktop App
企业内部 IDE
Server 可能每周更新。
如果协议没有:
Version
Optional Field
Feature Discovery
Backward Compatibility
每次服务端升级都可能把旧客户端打坏。
所以协议对象最好遵循:
新增字段优先
旧字段保留兼容期
未知字段客户端忽略
Breaking Change 升版本
Codex 使用 JSONL over stdio 的原因
OpenAI 说明里特别提到,它是一个“JSON-RPC lite”:保留 Request / Response / Notification 的形状,但不严格使用标准 JSON-RPC 2.0 Header,并通过 JSONL over stdio framing。
本地 IDE 很适合这种模式:
IDE
↓ spawn
App Server Child Process
↓ stdin/stdout
JSONL
优势:
不需要额外端口
权限边界简单
进程生命周期容易绑定
日志容易隔离
本地集成最重要的一件事:Pin Binary Version
OpenAI 的 VS Code Extension 和 Desktop App 会打包经过测试的平台二进制,确保客户端运行的是验证过的 App Server 版本。
这个细节非常重要。
不要:
IDE v1.8
自动连接系统里随机的 codex 最新版
应该:
Client Version
→ Tested Server Version Range
例如:
app_server:
min_version: 2.4.0
tested_version: 2.6.1
max_compatible: 2.x
Web 场景为什么必须 Server-side State
OpenAI 的 Web Runtime 会:
Worker
→ Provision Container
→ Checkout Workspace
→ Launch App Server
→ 持久通道
浏览器 Tab 不是 Source of Truth。
因为:
Tab 会关
网络会断
笔记本会休眠
长任务必须继续。
所以:
State
Progress
Thread Event
保存在 Server。
新浏览器 Session 连接以后:
Replay Missing Events
→ Catch Up
我会给事件增加 Sequence Number
{
"thread_id": "th-81",
"seq": 184,
"method": "item/completed",
"params": {}
}
客户端保存:
last_seq = 177
重连:
GET /threads/th-81/events?after=177
避免断线期间丢事件。
Event Replay 必须幂等
客户端可能重复收到:
seq=184
UI Projection 应该按:
thread_id + seq
去重。
不要重复创建 Artifact 或重复弹 Approval。
MCP 和 App Server 不是二选一
OpenAI 也明确区分了两者。
如果你已经有 MCP Workflow,只想:
把 Codex 当一个 Tool 调用
codex mcp-server 很适合。
但 MCP 暴露的是通用工具语义。
它不一定能完整表达:
Diff Stream
Thread Lifecycle
Rich Approval
Session-specific Events
如果要做完整 IDE 体验,App Server 更适合。
Cross-provider Protocol 也会遇到“最小公分母”问题
为了同时兼容多个 Agent Provider,协议往往只保留大家都有的功能。
最终可能变成:
message
call_tool
result
但某个 Provider 有:
Diff Artifact
Approval Resume
Fork Thread
Special Tool State
就很难表达。
所以选协议时要先问:
我要的是可移植性
还是完整 Harness 能力?
三种集成方式,我会这样选
CI 一次性任务
Codex Exec
特点:
单命令
跑完退出
明确 Exit Code
TypeScript Server Integration
Codex SDK
不用自己写完整 JSON-RPC Binding。
IDE / Desktop / Rich Agent UI
App Server
因为需要:
Thread
Streaming
Diff
Approval
Resume
一个 Spring Boot Client 骨架
虽然 App Server 通常作为本地子进程运行,Java 客户端也可以做一个 Process Adapter。
public final class CodexAppServerProcess {
private final Process process;
private final BufferedWriter writer;
private final BufferedReader reader;
public CodexAppServerProcess(
Path binary) throws IOException {
this.process = new ProcessBuilder(
binary.toString(),
"app-server")
.redirectErrorStream(false)
.start();
this.writer = new BufferedWriter(
new OutputStreamWriter(
process.getOutputStream()));
this.reader = new BufferedReader(
new InputStreamReader(
process.getInputStream()));
}
}
发送 JSONL:
public synchronized void send(
JsonNode request) throws IOException {
writer.write(request.toString());
writer.newLine();
writer.flush();
}
Reader 独立线程持续消费 Event。
不要在 Reader 线程里直接做业务
差:
read line
→ parse
→ update DB
→ call UI
→ approve logic
一个慢操作会阻塞整个事件流。
正确:
Reader
→ Parse
→ Event Queue
→ Consumers
Reader 只负责快速收包。
每个 Thread 独立 Mailbox
Map>
避免某个 Thread 的大 Diff 阻塞其他任务。
如果单进程承载多个 Thread,还要限制:
Maximum Threads
Event Queue Size
Per-thread Memory
Backpressure 必须有
Tool 输出可能非常大。
例如:
mvn test
产生几十 MB 日志。
不要把全部 delta 无限制塞进内存。
可以:
UI Stream:最后 N KB
完整日志:Artifact Store
例如:
stream:
max_buffer_kb: 512
full_log_to_artifact: true
Diff 同样不要只当文本
应该保存:
Base Commit
File Path
Patch Hash
Generated By Turn
例如:
public record DiffArtifact(
String artifactId,
String threadId,
String turnId,
String baseCommit,
String patchRef,
String sha256) {
}
这样重连、Review 和审计都能复用。
一个 Agent Harness Protocol 最少要能回答 8 个问题
1. Thread 怎么创建和恢复?
2. 一次用户请求的 Turn 边界在哪?
3. 中间步骤如何流式传输?
4. Tool 和 Diff 怎么表示?
5. Approval 怎么暂停和恢复?
6. 断线后怎么 Catch Up?
7. Client/Server 版本怎么协商?
8. 如何取消一个正在执行的 Turn?
如果协议只有:
send_message
receive_message
做复杂 Agent UI 很快就会补丁遍地。
Codex App Server 最值得借鉴的不是 JSON-RPC 这个技术选型本身,而是:它没有把 Agent 当成一个更长的 Chat API。
Thread、Turn、Item、Approval、Diff 和 Event Lifecycle 都是明确协议对象。
这让:
IDE
Web
Desktop
CLI
可以复用同一 Harness,而不是每个客户端重新实现一遍 Agent Loop。
如果你正在做自己的 Agent 平台,我会优先把协议原语设计清楚,再决定到底用 WebSocket、SSE、stdio 还是 JSON-RPC。
传输层可以换。
Thread、Turn、Item 和可恢复事件语义一旦设计错,后面很难补。
更多企业级 AI 应用、Agent、RAG 与大模型工程化内容,我会继续整理在 智元界:
https://www.zyentor.com/