第二章讲的是协议设计:为什么是两条消息河流、事件为什么分层。本附录是对接手册: 如果你要自己写一个前端(IDE 插件、Web 面板、聊天机器人、自动化脚本……)接入 Codex, 这一章给出完整的消息目录、字段约定、时序和最小客户端骨架。
对外对接的标准入口是 app-server:一个独立进程,说 JSON-RPC 风格的消息。 TUI 内部跑的是同一套语义(只是传输换成内存通道),所以学会本附录,你就掌握了所有前端形态的共同语言。
约定:消息名、字段名一律用线上原名(英文,camelCase);解释性文字用中文。标注 [EXP] 的方法/字段需要在握手时声明实验能力,见 A.3。
A.1 启动与传输
启动一个 app-server 进程(codex app-server),支持三种监听方式:
| 启动方式 | 传输形态 | 适用场景 |
|---|---|---|
默认 / --stdio(或 --listen stdio://) |
标准输入输出上跑 JSONL:一行一条 JSON 消息 | 本地 IDE 插件、CLI 内嵌(最常用) |
--listen ws://127.0.0.1:PORT |
WebSocket:一个 text frame 一条消息;同端口有 GET /healthz、GET /readyz |
浏览器/多客户端/远程面板(实验性) |
--listen unix:// |
Unix domain socket 上跑 WebSocket 握手 | 本机多进程共享(默认 socket 位于 $CODEX_HOME/app-server-control/) |
--listen off |
不暴露端口 | 只想要进程内形态时 |
要点:
- 消息是 JSON-RPC 2.0 风格,但不带
"jsonrpc": "2.0"字段; - stdio 模式下一行就是一条消息(换行分隔,不要在消息内换行);
- 字段命名统一 camelCase;时间戳:通知里用 Unix 毫秒(
startedAtMs),线程/轮次元数据用 Unix 秒(createdAt); - 进程随父进程生死:父进程退出,子进程被回收。
A.2 消息信封:四种消息
线上只有四种消息,靠 id 和 method 字段区分:
// ① 请求(有 id,必须回响应)
{ "id": 30, "method": "turn/start", "params": { ... } }
// ② 通知(无 id,不回响应)—— 服务端 → 客户端方向最常见
{ "method": "turn/started", "params": { ... } }
// ③ 成功响应
{ "id": 30, "result": { "turn": { ... } } }
// ④ 错误响应
{ "id": 30, "error": { "code": -32600, "message": "...", "data": { ... } } }
id:字符串或整数,由发起方分配,响应原样带回;建议用自增整数;- 双向:客户端给服务端发请求(方法调用),服务端也会给客户端发请求(审批、提问,见 A.10)——后者同样有
id,客户端必须回响应; - 请求可选带
trace字段传播 W3C 追踪上下文; - 特殊错误码:-32001 = 服务端正忙(队列饱和),客户端应稍后重试;握手前调用返回 “Not initialized”,重复握手返回 “Already initialized”。
A.3 连接生命周期:握手、订阅、退订
sequenceDiagram
participant C as 客户端
participant S as app-server
C->>S: 请求 initialize(clientInfo + capabilities)
S-->>C: 响应(userAgent / codexHome / 平台信息)
C->>S: 通知 initialized(无参数)
Note over C,S: 之后才允许其他请求
C->>S: 请求 thread/start(或 thread/resume)
S-->>C: 响应 { thread }
S-->>C: 通知 thread/started(含 thread.status)
Note over C,S: 本连接自动订阅该线程的全部通知
C->>S: 请求 turn/start(用户输入)
S-->>C: 响应 { turn: { status: "inProgress" } }
S-->>C: 通知 turn/started → item/* → turn/completed
C->>S: 请求 thread/unsubscribe(可选)
Note over S: 最后一个订阅者离开后,线程保留约 30 分钟<br/>空闲后卸载,发 thread/closed
握手请求:
{
"method": "initialize",
"id": 0,
"params": {
"clientInfo": { "name": "my_client", "title": "My Client", "version": "0.1.0" },
"capabilities": {
"experimentalApi": true, // 想用 [EXP] 方法时开启
"requestAttestation": false, // 桌面宿主可同意生成 attestation
"optOutNotificationMethods": [ // 按精确方法名屏蔽不想要的通知
"item/agentMessage/delta"
]
// "extensions": { ... } // MCP 扩展声明(如表单能力)
}
}
}
clientInfo.name用于合规日志识别客户端,接入方应起稳定的名字;optOutNotificationMethods是精确方法名数组(无通配),可用来压低流量(比如不渲染打字机效果就可以屏蔽 delta 类通知);- 未声明
experimentalApi时调用 [EXP] 方法会收到错误:<reason> requires experimentalApi capability;实验字段在输出时也可能被裁剪; thread/start、thread/resume、thread/fork会自动订阅当前连接;thread/unsubscribe显式退订;线程在最后一个订阅者离开后保留约 30 分钟无活动才卸载(跑收尾 hooks,发thread/closed)。
A.4 对象模型:thread → turn → item
app-server 的世界只有三个核心对象,通知和查询都围绕它们:
| 对象 | 含义 | 标识 |
|---|---|---|
| thread | 一段持续会话(对应内核的线程/会话),持久化在磁盘上 | threadId(如 thr_123) |
| turn | 一轮“用户输入 → agent 回复” | turnId(如 turn_456) |
| item | turn 内的一个个条目:一条消息、一次命令执行、一次文件改动…… | itemId |
turn 状态(turn.status):
| 值 | 含义 |
|---|---|
inProgress |
运行中 |
completed |
正常完成 |
interrupted |
被中断(用户中断;turn/interrupt 的终态) |
failed |
出错终止(错误在 turn.error) |
thread 状态(thread.status,判别对象):
notLoaded:未载入内存(列表里的历史线程默认如此);idle:已载入、空闲;active:有轮次在跑,附带activeFlags:waitingOnApproval(等审批)、waitingOnUserInput(等用户回答);systemError:线程出错。
item 类型(item.type,判别联合,渲染层按此分发 UI):
| type | 是什么 |
|---|---|
userMessage |
用户消息(turn/start 的输入回显) |
agentMessage |
assistant 的最终回复 |
reasoning |
reasoning(思考摘要,可能含多个分段) |
plan |
任务计划清单 |
commandExecution |
一次命令执行(含命令、cwd、状态、输出) |
fileChange |
一次文件改动(补丁/差异) |
mcpToolCall |
一次 MCP 工具调用 |
dynamicToolCall |
一次动态工具调用(执行方可能是客户端) |
webSearch / imageGeneration / imageView / sleep |
联网搜索 / 图片生成 / 看图 / 等待 |
subAgentActivity |
子 agent 活动 |
collabAgentToolCall |
协作模式 agent 调用 |
hookPrompt |
hook 注入的提示 |
enteredReviewMode / exitedReviewMode |
进入/退出代码审查模式 |
contextCompaction |
上下文压缩记录 |
item 自身状态(commandExecution / fileChange 等):inProgress → completed / failed / declined(被用户拒绝)。
A.5 最小会话流程(端到端时序)
一次完整对话的消息序列(客户端视角):
sequenceDiagram
participant C as 客户端
participant S as app-server
C->>S: turn/start { threadId, input }
S-->>C: { turn: { id, status: "inProgress", items: [] } }
S-->>C: 通知 turn/started
S-->>C: 通知 item/started(userMessage,回显输入)
S-->>C: 通知 item/completed(userMessage)
S-->>C: 通知 item/started(reasoning)
S-->>C: 通知 item/reasoning/summaryTextDelta ×N(思考流)
S-->>C: 通知 item/completed(reasoning)
S-->>C: 通知 item/started(commandExecution,inProgress)
S-->>C: 请求 item/commandExecution/requestApproval(id=R1)
Note over C: 弹审批框
C-->>S: 响应 R1 { decision: "accept" }
S-->>C: 通知 serverRequest/resolved { requestId: R1 }
S-->>C: 通知 item/commandExecution/outputDelta ×N(命令输出流)
S-->>C: 通知 item/completed(commandExecution,completed)
S-->>C: 通知 item/started(agentMessage)
S-->>C: 通知 item/agentMessage/delta ×N(回复打字机)
S-->>C: 通知 item/completed(agentMessage)
S-->>C: 通知 turn/completed(status: "completed" + token usage)
三条渲染铁律(第二章 2.4.5/2.8 的协议化版本):
- item 是权威,delta 是加速带。UI 的数据模型以
item/completed为准重建;delta 只用来拼“正在生成”的临时气泡。断线重连/迟到订阅后,用thread/items/list或turn/completed里的 items 对齐,delta 漏了不补发; - 所有通知带坐标:
threadId/turnId/itemId,按坐标归位到对应线程面板; - turn 的终态只看
turn/completed。turn/interrupt的响应只表示“已受理”,轮次真正结束以turn/completed(status: "interrupted")为准。
A.6 客户端 → 服务端方法速查
只列对接常用方法;完整字段以随包发布的 JSON Schema / TypeScript 类型为准。
会话与轮次(最常用)
| 方法 | 作用 | 关键 params / result |
|---|---|---|
initialize |
握手 | params:clientInfo{name,title?,version?}、capabilities?;result:userAgent、codexHome、platformFamily、platformOs |
thread/start |
新建线程 | params:cwd?、model?、approvalPolicy?、sandbox?/sandboxPolicy?、personality? 等;result:{ thread };随后有 thread/started 通知并自动订阅 |
thread/resume |
恢复历史线程 | params:threadId(+ 可选策略覆盖);result:{ thread } |
thread/fork |
分叉线程 | params:threadId、lastTurnId?、ephemeral?;result:{ thread }(含 forkedFromId) |
thread/list |
线程列表(游标分页) | params:cursor?、limit?、过滤项;result:{ data, nextCursor } |
thread/read |
读线程(不恢复) | params:threadId、includeTurns? |
thread/loaded/list |
当前内存中的线程 | — |
thread/unsubscribe |
退订通知 | params:threadId |
thread/archive / unarchive / delete |
归档/恢复/删除 | params:threadId |
turn/start |
发起一轮 | 见 A.7;result:{ turn: { id, status, items, error } } |
turn/steer |
插话(见第一章 1.6) | params:threadId、input、expectedTurnId(必填)、clientUserMessageId?;result:{ turnId } |
turn/interrupt |
中断当前轮 | params:threadId、turnId;result:{};终态等 turn/completed |
review/start |
代码审查 | params:threadId、target、delivery: "inline"/"detached" |
thread/compact/start |
手动压缩上下文 | params:threadId |
thread/shellCommand |
跑一次性 !命令 |
params:threadId、command |
thread/rollback(废弃) |
丢弃最近 N 个用户轮次的历史(不动磁盘文件) | params:threadId、numTurns;result:更新后的 thread |
thread/revert [EXP] |
新版回滚:把历史替换为某个 turn 之前的前缀 | params:threadId、beforeTurnId |
thread/inject_items |
往历史注入原始 item(不开轮次) | params:threadId、items(Responses API item 格式) |
配置 / 模型 / 功能
| 方法 | 作用 |
|---|---|
config/read / config/value/write / config/batchWrite |
读/写 config.toml(写支持 reloadUserConfig 热加载) |
configRequirements/read |
托管环境(MDM/requirements.toml)的强制约束 |
model/list |
可用模型目录 |
modelProvider/capabilities/read |
provider 能力 |
experimentalFeature/list / experimentalFeature/enablement/set |
实验特性开关(带 stable/beta/underDevelopment 阶段标记) |
permissionProfile/list |
可用权限 profile |
config/mcpServer/reload / mcpServerStatus/list |
MCP 重载/状态 |
mcpServer/oauth/login / mcpServer/resource/read / mcpServer/tool/call |
MCP 登录/资源/工具直调 |
扩展生态
| 方法 | 作用 |
|---|---|
skills/list / skills/config/write / skills/extraRoots/set |
skills |
hooks/list |
hooks 清单与信任状态 |
plugin/list / plugin/search [EXP] / plugin/installed / plugin/install / plugin/uninstall / plugin/read / plugin/skill/read |
插件 |
marketplace/add / remove / upgrade |
插件市场 |
app/list / app/read / app/installed |
连接器应用 |
文件系统与进程
| 方法 | 作用 |
|---|---|
fs/readFile / writeFile / readDirectory / getMetadata / createDirectory / remove / copy |
文件操作(路径用 file URI) |
fs/watch / fs/unwatch |
目录监听,变更走 fs/changed 通知(自带 watchId) |
command/exec(+ /write、/terminate、/resize) |
独立命令会话,输出走 command/exec/outputDelta |
process/spawn / writeStdin / kill / resizePty [EXP] |
通用 PTY 进程 |
账户与其他
| 方法 | 作用 |
|---|---|
account/login/start / login/cancel / logout / account/read |
登录与账户 |
account/rateLimits/read / account/usage/read |
配额与用量 |
feedback/upload |
反馈 |
thread/queue/* [EXP] |
持久化的用户消息队列(空闲时自动 FIFO 提交) |
thread/backgroundTerminals/list / clean / terminate [EXP] |
后台终端管理 |
thread/realtime/* [EXP] |
实时语音(start/appendAudio/appendText/stop/listVoices) |
environment/* [EXP]、remoteControl/* [EXP]、project/* [EXP]、server/diagnostics [EXP] |
远程环境 / 远程控制 / 项目分组 / 诊断 |
分页约定:列表方法统一
params: { cursor?, limit? }→result: { data: [...], nextCursor: string | null }。
A.7 turn/start 请求体详解
{
"method": "turn/start",
"id": 30,
"params": {
"threadId": "thr_123",
"clientUserMessageId": "client_msg_123", // 可选:客户端自己的消息幂等 ID
"input": [
{ "type": "text", "text": "帮我跑一下测试" }
// 也可附带 { "type": "skill", "name", "path" } —— 显式调用 skill
// 或 { "type": "mention", "name", "path" } —— @插件 / $应用
],
// —— 以下都是可选的本轮覆盖项 ——
"cwd": "/Users/me/project",
"model": "gpt-5.1-codex",
"effort": "medium", // reasoning effort
"summary": "concise", // reasoning summary 详细度
"personality": "friendly", // friendly | pragmatic | none
"approvalPolicy": "unlessTrusted", // 审批策略
"sandboxPolicy": { // 沙箱(旧简写 "sandbox": "workspaceWrite")
"type": "workspaceWrite",
"writableRoots": ["/Users/me/project"],
"networkAccess": true
},
// "permissions": ":workspace", // [EXP] 推荐用权限 profile id,与 sandboxPolicy 二选一
"outputSchema": { // 可选:约束最终回复为结构化 JSON
"type": "object",
"properties": { "answer": { "type": "string" } },
"required": ["answer"],
"additionalProperties": false
}
}
}
响应是即时的(第二章 2.3 的“确认即回”):
{ "id": 30, "result": { "turn": {
"id": "turn_456",
"status": "inProgress",
"items": [],
"error": null
} } }
之后的一切通过通知到达。turn/steer 字段类似但必须带 expectedTurnId(防止串台),且不接受策略覆盖、不产生新的 turn/started。
A.8 服务端 → 客户端通知速查
线程与轮次生命周期
| 通知 | 时机 / 载荷要点 |
|---|---|
thread/started |
线程载入完成(start/resume/fork 后),含完整 thread 对象 |
thread/status/changed |
thread.status 变化(如 idle → active) |
thread/closed |
线程卸载(最后订阅者离开约 30 分钟后) |
thread/archived / unarchived / deleted |
归档/恢复/删除 |
thread/tokenUsage/updated |
token 用量(fork 回放历史用量也用它) |
thread/name/updated、thread/goal/updated / cleared |
重命名、长期目标变化 |
turn/started |
轮次真正开始运行 |
turn/completed |
轮次终态:完整 turn 对象(status、items、error、token usage) |
turn/diff/updated |
本轮累计代码 diff 更新 |
turn/plan/updated |
计划更新 |
hook/started / hook/completed |
hook 执行进度 |
item 生命周期与流式 delta
| 通知 | 时机 |
|---|---|
item/started |
新 item 出现(载荷 { threadId, turnId, item, startedAtMs }) |
item/completed |
item 完成(权威终态,渲染以它为准) |
item/agentMessage/delta |
assistant 文本增量 { itemId, delta } |
item/reasoning/summaryTextDelta |
reasoning summary 增量 |
item/reasoning/summaryPartAdded |
reasoning 新分段(标题块) |
item/reasoning/textDelta |
加密 raw reasoning 增量 |
item/plan/delta |
计划文本增量 |
item/commandExecution/outputDelta |
命令输出增量 |
item/commandExecution/terminalInteraction |
交互式终端的回显 |
item/fileChange/patchUpdated |
补丁流式预览 |
item/mcpToolCall/progress |
MCP 工具进度 |
item/autoApprovalReview/started / completed |
自动审批审查进度 |
状态与旁路
| 通知 | 含义 |
|---|---|
error / warning / guardianWarning |
错误/警告(error 完整载荷见 A.9.4 与 A.11) |
deprecationNotice |
某功能将废弃 |
model/rerouted |
服务端把请求改道到了别的模型 |
model/safetyBuffering/updated |
安全审查缓冲状态 |
model/verification |
账号验证建议 |
serverRequest/resolved |
一个反向请求已被解决或清理(见 A.10) |
fs/changed、account/updated、account/rateLimits/updated、app/list/updated、skills/changed、mcpServer/startupStatus/updated |
各类资源变更广播 |
rawResponseItem/completed、rawResponse/completed |
内部/高级:原始模型 item 与精确 usage 透传(普通前端不需要) |
A.9 服务端 → 客户端通知:完整 JSON 示例
A.8 是通知目录,本节给出可直接对照实现的载荷示例。通知都是无 id 的 JSON-RPC 通知:{ "method": ..., "params": { ... } }。
字段名即线上字段(camelCase);示例值是虚构但形状真实的。所有通知都带 threadId/turnId 坐标(少数全局通知除外),用它归位到对应线程。
A.9.1 一个完整 turn 的通知流(JSONL)
下面是用户说“跑一下测试”,agent 思考 → 申请执行命令 → 执行 → 回复,整个过程客户端收到的通知序列:
// ① 轮次真正开始运行(turn/start 的即时响应之后,才来这条)
{ "method": "turn/started", "params": {
"threadId": "thr_123",
"turn": {
"id": "turn_456",
"status": "inProgress",
"items": [],
"itemsView": "full",
"error": null,
"startedAt": 1788516346,
"completedAt": null,
"durationMs": null
}
} }
// ② 用户消息回显(item 生命周期:started → completed)
{ "method": "item/started", "params": {
"threadId": "thr_123", "turnId": "turn_456", "startedAtMs": 1788516346100,
"item": {
"type": "userMessage",
"id": "item_01",
"clientId": "client_msg_123", // 来自 turn/start 的 clientUserMessageId
"content": [ { "type": "text", "text": "跑一下测试" } ]
}
} }
{ "method": "item/completed", "params": {
"threadId": "thr_123", "turnId": "turn_456", "completedAtMs": 1788516346102,
"item": {
"type": "userMessage",
"id": "item_01",
"clientId": "client_msg_123",
"content": [ { "type": "text", "text": "跑一下测试" } ]
}
} }
// ③ reasoning 开始,思考摘要流式到达
{ "method": "item/started", "params": {
"threadId": "thr_123", "turnId": "turn_456", "startedAtMs": 1788516347200,
"item": { "type": "reasoning", "id": "item_02", "summary": [], "content": [] }
} }
{ "method": "item/reasoning/summaryPartAdded", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_02", "summaryIndex": 0
} }
{ "method": "item/reasoning/summaryTextDelta", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_02",
"summaryIndex": 0, "delta": "用户要求运行测试。"
} }
{ "method": "item/reasoning/summaryTextDelta", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_02",
"summaryIndex": 0, "delta": "项目是 Rust,用 cargo test。"
} }
// reasoning 完成:summary 是分段字符串数组,content 是 raw reasoning(开源模型才有)
{ "method": "item/completed", "params": {
"threadId": "thr_123", "turnId": "turn_456", "completedAtMs": 1788516351800,
"item": {
"type": "reasoning", "id": "item_02",
"summary": [ "用户要求运行测试。项目是 Rust,用 cargo test。" ],
"content": []
}
} }
// ④ 命令执行 item 出现(inProgress),随后触发审批——审批是【反向请求】,见 A.10
{ "method": "item/started", "params": {
"threadId": "thr_123", "turnId": "turn_456", "startedAtMs": 1788516352000,
"item": {
"type": "commandExecution",
"id": "item_03",
"command": "cargo test",
"cwd": "/Users/me/project",
"processId": null,
"source": "agent",
"status": "inProgress",
"commandActions": [], // 对命令意图的结构化解析(读/写/执行等),可能为空数组
"aggregatedOutput": null,
"exitCode": null,
"durationMs": null
}
} }
// 审批请求是反向请求(带 id,需响应)——完整载荷见 A.10.1
{ "method": "item/commandExecution/requestApproval", "id": "req_1", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_03",
"startedAtMs": 1788516352010, "environmentId": "local", "approvalId": null,
"command": "cargo test", "cwd": "/Users/me/project", "commandActions": [],
"reason": "命令需要在工作区执行"
} }
// 客户端回 {"id":"req_1","result":{"decision":"accept"}} 后:
{ "method": "serverRequest/resolved", "params": { "threadId": "thr_123", "requestId": "req_1" } }
// ⑤ 命令输出流式到达(delta 是纯文本,拼接即终端输出)
{ "method": "item/commandExecution/outputDelta", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_03",
"delta": " Compiling codex v0.1.0\n"
} }
{ "method": "item/commandExecution/outputDelta", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_03",
"delta": " Finished test result: ok. 42 passed\n"
} }
// 命令 item 完成:终态、退出码、耗时、聚合输出(权威结果)
{ "method": "item/completed", "params": {
"threadId": "thr_123", "turnId": "turn_456", "completedAtMs": 1788516360500,
"item": {
"type": "commandExecution", "id": "item_03",
"command": "cargo test", "cwd": "/Users/me/project",
"processId": null, "source": "agent",
"status": "completed", // completed | failed | declined
"commandActions": [],
"aggregatedOutput": " Compiling codex v0.1.0\n Finished test result: ok. 42 passed\n",
"exitCode": 0,
"durationMs": 8490
}
} }
// ⑥ assistant 最终回复,打字机式 delta
{ "method": "item/started", "params": {
"threadId": "thr_123", "turnId": "turn_456", "startedAtMs": 1788516360700,
"item": { "type": "agentMessage", "id": "item_04", "text": "", "phase": null, "memoryCitation": null, "delivery": null }
} }
{ "method": "item/agentMessage/delta", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_04", "delta": "测试全部通过"
} }
{ "method": "item/agentMessage/delta", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_04", "delta": ",42 个用例 ok。"
} }
// agentMessage 完成:text 是拼接后的完整回复;phase: commentary/finalAnswer;delivery: "async" 表示不结束轮次的中途插话
{ "method": "item/completed", "params": {
"threadId": "thr_123", "turnId": "turn_456", "completedAtMs": 1788516362000,
"item": {
"type": "agentMessage", "id": "item_04",
"text": "测试全部通过,42 个用例 ok。",
"phase": "finalAnswer", "memoryCitation": null, "delivery": null
}
} }
// ⑦ 轮次终态(status: completed;turn 里只附最后的 agent 消息作摘要,完整列表以 item/* 为准)
{ "method": "turn/completed", "params": {
"threadId": "thr_123",
"turn": {
"id": "turn_456",
"status": "completed", // completed | interrupted | failed
"items": [ /* 通常只含最后一条 agentMessage 摘要;完整 item 流来自 item/completed */ ],
"itemsView": "full",
"error": null,
"startedAt": 1788516346,
"completedAt": 1788516362,
"durationMs": 16000
}
} }
// ⑧ token 用量单独一条(total 为线程累计,last 为本轮;上下文窗口大小用于画用量条)
{ "method": "thread/tokenUsage/updated", "params": {
"threadId": "thr_123", "turnId": "turn_456",
"tokenUsage": {
"total": { "totalTokens": 12840, "inputTokens": 11020, "cachedInputTokens": 9800,
"cacheWriteInputTokens": 0, "outputTokens": 1820, "reasoningOutputTokens": 640 },
"last": { "totalTokens": 2100, "inputTokens": 1700, "cachedInputTokens": 1200,
"cacheWriteInputTokens": 0, "outputTokens": 400, "reasoningOutputTokens": 220 },
"modelContextWindow": 272000
}
} }
要点回顾:item/started → 若干专属 delta → item/completed 是每个 item 的固定节奏;UI 以 item/completed 和 turn/completed 为权威,delta 只做实时拼接。
A.9.2 文件改动、计划与压缩
fileChange item(apply_patch 类工具)与流式补丁预览:
// 补丁边生成边给结构化预览(changes 是当前累计快照,不是增量)
{ "method": "item/fileChange/patchUpdated", "params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_05",
"changes": [
{ "path": "src/lib.rs", "kind": "update",
"diff": "@@ -10,3 +10,4 @@\n pub fn add(a: i32, b: i32) -> i32 {\n- a + b\n+ a + b + 0\n+ // 新的一行\n" }
]
} }
{ "method": "item/completed", "params": {
"threadId": "thr_123", "turnId": "turn_456", "completedAtMs": 1788516400000,
"item": {
"type": "fileChange", "id": "item_05",
"changes": [
{ "path": "src/lib.rs", "kind": "update", "diff": "@@ ..." }
// kind: "add"(新增文件)| "delete"(删除)| "update"(修改,rename 时带 movePath)
],
"status": "completed" // inProgress | completed | failed | declined(用户拒绝)
}
} }
// 本轮累计 diff 的整图快照(不用自己拼 fileChange)
{ "method": "turn/diff/updated", "params": {
"threadId": "thr_123", "turnId": "turn_456",
"diff": "diff --git a/src/lib.rs b/src/lib.rs\n..."
} }
// 计划模式:plan item + 结构化 plan 更新
{ "method": "turn/plan/updated", "params": {
"turnId": "turn_456",
"explanation": "我先跑测试再修回归",
"plan": [
{ "step": "运行测试定位失败", "status": "completed" },
{ "step": "修复回归", "status": "inProgress" },
{ "step": "重跑验证", "status": "pending" }
]
} }
// 上下文被压缩(自动或手动)时出现,只有 id;历史在此被摘要替换
{ "method": "item/completed", "params": {
"threadId": "thr_123", "turnId": "turn_456", "completedAtMs": 1788516500000,
"item": { "type": "contextCompaction", "id": "item_09" }
} }
A.9.3 线程生命周期与模型旁路通知
// thread/started:线程载入完成(start/resume/fork 后),thread 是完整对象
{ "method": "thread/started", "params": {
"thread": {
"id": "thr_123", "sessionId": "sess_abc", "forkedFromId": null, "parentThreadId": null,
"preview": "跑一下测试", "ephemeral": false,
"modelProvider": "openai", "cwd": "/Users/me/project", "cliVersion": "0.0.0",
"source": "vscode", "createdAt": 1788516340, "updatedAt": 1788516340, "recencyAt": 1788516340,
"status": { "type": "active", "activeFlags": [] },
"turns": []
}
} }
// 状态变化:空闲 ↔ 活跃(活跃时可能带 waitingOnApproval / waitingOnUserInput)
{ "method": "thread/status/changed", "params": {
"threadId": "thr_123",
"status": { "type": "active", "activeFlags": ["waitingOnApproval"] }
} }
{ "method": "thread/status/changed", "params": {
"threadId": "thr_123",
"status": { "type": "idle" }
} }
// 最后一个订阅者离开约 30 分钟后,线程卸载
{ "method": "thread/closed", "params": { "threadId": "thr_123" } }
// 服务端把请求改道到另一个模型(如高风险安全检查触发)
{ "method": "model/rerouted", "params": {
"threadId": "thr_123", "turnId": "turn_456",
"fromModel": "gpt-5.1-codex", "toModel": "gpt-5-codex",
"reason": "highRiskCyberActivity"
} }
// 安全审查缓冲:输出被暂存,UI 可显示"审查中";可能换更快的模型
{ "method": "model/safetyBuffering/updated", "params": {
"threadId": "thr_123", "turnId": "turn_456",
"model": "gpt-5.1-codex",
"useCases": ["cyber_safety"], "reasons": ["policy_review"],
"showBufferingUi": true, "fasterModel": null
} }
A.9.4 错误与警告通知
// 瞬时故障(断流/限流):willRetry=true,轮次没死,UI 只提示"重连中",不要改状态
{ "method": "error", "params": {
"threadId": "thr_123", "turnId": "turn_456", "willRetry": true,
"error": {
"message": "Reconnecting... 2/5",
"codexErrorInfo": { "responseStreamDisconnected": { "httpStatusCode": 200 } },
"additionalDetails": "stream closed before response.completed"
}
} }
// 终态错误:willRetry=false;同样内容会出现在随后的 turn/completed(status:"failed")
{ "method": "error", "params": {
"threadId": "thr_123", "turnId": "turn_456", "willRetry": false,
"error": {
"message": "Usage limit reached",
"codexErrorInfo": "usageLimitExceeded"
}
} }
// 非致命警告(线程相关时带 threadId)
{ "method": "warning", "params": { "threadId": "thr_123", "message": "部分已启用的 skill 未列入本会话的模型可见列表" } }
// 配置类诊断(初始化或 thread/start 时的 exec-policy 解析问题)
{ "method": "configWarning", "params": {
"summary": "config.toml 中有无法识别的字段",
"details": "unknown field `foo` at line 12",
"path": "/Users/me/.codex/config.toml"
} }
codexErrorInfo 的常见取值(字符串形式或带 HTTP 状态码的对象形式):
contextWindowExceeded、sessionBudgetExceeded、usageLimitExceeded、serverOverloaded、
cyberPolicy、misalignmentPolicyViolation、badRequest、unauthorized、sandboxError、
internalServerError、other;对象形式有 httpConnectionFailed、responseStreamConnectionFailed、
responseStreamDisconnected、responseTooManyFailedAttempts(均带 httpStatusCode,可为 null)、
activeTurnNotSteerable(带 turnKind: "review" | "compact")。
反向请求(审批、提问、表单、动态工具)也是服务端 → 客户端,但它们带
id、必须应答,不是通知。完整载荷与应答格式见下一节 A.10。
A.10 反向请求:服务端向客户端“要东西”
这是对接时最容易漏掉的部分:服务端也会发带 id 的请求,客户端必须响应(第二章 2.3 的“问与答”)。轮次结束/中断时未决请求会被服务端自动中止,并发 serverRequest/resolved 清理。
| 方法 | 触发场景 | 客户端应答 |
|---|---|---|
item/commandExecution/requestApproval |
命令需审批 | { "decision": ... },见下表 |
item/fileChange/requestApproval |
文件改动需审批 | { "decision": "accept" | "acceptForSession" | "decline" | "cancel" } |
item/permissions/requestApproval |
工具申请额外权限(网络/路径) | { permissions, scope: "turn"|"session" } |
item/tool/requestUserInput |
agent 向用户提问 | 按请求的 schema 返回答案;含 isBlocking 标识 |
mcpServer/elicitation/request |
MCP 服务器弹表单/URL | { action: "accept"|"decline"|"cancel", content? } |
item/tool/call |
动态工具交给客户端执行 | 返回工具执行结果 |
attestation/generate |
上游需要客户端证明(需握手声明 requestAttestation) |
{ token: "v1.<opaque>" } |
currentTime/read [EXP] |
外部时钟模式下读时间 | { currentTimeAt: <Unix 秒> } |
account/chatgptAuthTokens/refresh |
刷新 ChatGPT 令牌 | 新令牌 |
命令审批的 decision 取值(文件改动审批是其子集):
| decision | 含义 |
|---|---|
"accept" |
本次允许 |
"acceptForSession" |
本线程内同类操作不再询问 |
"decline" |
拒绝,但轮次继续(模型会看到拒绝结果) |
"cancel" |
拒绝并立即中断轮次 |
{ "acceptWithExecpolicyAmendment": { ... } } |
允许并把该命令前缀沉淀为持久规则 |
{ "applyNetworkPolicyAmendment": { ... } } |
允许网络访问并沉淀域名规则 |
审批消息序列(以命令为例):
sequenceDiagram
participant S as app-server
participant C as 客户端
S-->>C: 通知 item/started(commandExecution, inProgress)
S-->>C: 请求 item/commandExecution/requestApproval(id=R1,含 command/cwd/reason)
Note over C: 展示命令与风险,用户选择
C->>S: 响应 R1 { "decision": "accept" }
S-->>C: 通知 serverRequest/resolved { requestId: "R1" }
S-->>C: 通知 item/commandExecution/outputDelta ×N
S-->>C: 通知 item/completed(commandExecution, status: completed)
A.10.1 反向请求的完整载荷
命令审批请求与应答(请求带 id,响应 id 原样带回):
// 服务端 → 客户端:请求(注意它是 request,有 id)
{
"method": "item/commandExecution/requestApproval",
"id": "req_1",
"params": {
"threadId": "thr_123",
"turnId": "turn_456",
"itemId": "item_03",
"startedAtMs": 1788516352010,
"environmentId": "local",
"approvalId": null,
"command": "cargo test",
"cwd": "/Users/me/project",
"commandActions": [],
"reason": "命令需要在工作区执行"
// [EXP] 可能还有 additionalPermissions(申请的沙箱权限)、
// networkApprovalContext(纯网络审批时)、availableDecisions(建议的可选项)、
// proposedExecpolicyAmendment / proposedNetworkPolicyAmendments(持久规则建议)
}
}
// 客户端 → 服务端:应答(id 必须与请求一致,包在 result 里)
{ "id": "req_1", "result": { "decision": "accept" } }
// 其他 decision 示例:
// { "id": "req_1", "result": { "decision": "acceptForSession" } } // 本线程不再问
// { "id": "req_1", "result": { "decision": "decline" } } // 拒绝但轮次继续
// { "id": "req_1", "result": { "decision": "cancel" } } // 拒绝并中断轮次
// { "id": "req_1", "result": { "decision": {
// "applyNetworkPolicyAmendment": { "networkPolicyAmendment": { "host": "example.com", "action": "allow" } }
// } } }
// 服务端随后发一条通知,表示该请求已结算(清理 UI 上的审批框)
{ "method": "serverRequest/resolved", "params": { "threadId": "thr_123", "requestId": "req_1" } }
向用户提问(item/tool/requestUserInput):
{
"method": "item/tool/requestUserInput",
"id": "req_2",
"params": {
"threadId": "thr_123", "turnId": "turn_456", "itemId": "item_07",
"isBlocking": true,
"questions": [ /* 按请求中的结构化问题渲染表单 */ ]
}
}
// 客户端按问题回填答案,用同一个 id 响应;轮次结束前未答会收到 serverRequest/resolved 清理
{ "id": "req_2", "result": { /* answers */ } }
MCP 表单(mcpServer/elicitation/request):
{
"method": "mcpServer/elicitation/request",
"id": "req_3",
"params": {
"threadId": "thr_123", "turnId": "turn_456",
"serverName": "github",
"mode": "form", // "form" | "openai/form" | "url"
"message": "授权访问 GitHub 仓库",
"requestedSchema": { /* JSON Schema,客户端据此渲染表单;不认识的字段要能回 decline */ }
}
}
// 应答:
// { "id": "req_3", "result": { "action": "accept", "content": { ... } } }
// { "id": "req_3", "result": { "action": "decline", "content": null } }
// { "id": "req_3", "result": { "action": "cancel", "content": null } }
可靠性提示:反向请求绝不允许不答。客户端无法处理时(比如不认识的表单),也要回
decline/cancel或错误响应,否则轮次会永远挂起。过载时普通通知可能被丢弃,但反向请求会失败返回而不是静默消失。
A.11 错误处理
两层错误,别混淆:
1. RPC 层错误(请求本身失败):{ "id": <id>, "error": { "code", "message", "data?" } }
-32001:服务端过载,退避重试;- 握手前 / 重复握手:“Not initialized” / “Already initialized”;
turn/steer目标轮次不存在或不可插话:invalid request 类错误;- 未开实验能力调 [EXP] 方法:
requires experimentalApi capability。
2. turn 层错误(轮次跑起来之后失败):走 error 通知:
{
"method": "error",
"params": {
"threadId": "thr_123",
"turnId": "turn_456",
"willRetry": true, // true = 瞬时故障(断流/限流),app-server 正在自动重试,轮次没死
"error": {
"message": "Reconnecting... 2/5",
"codexErrorInfo": "responseStreamDisconnected", // 可选,机器可读分类
"additionalDetails": "..."
}
}
}
处理规则:
willRetry: true不要动 UI 状态:这是“重连中”提示(第二章 2.4.5),轮次仍在 inProgress,重试成功后 delta 流继续;- 终态错误出现在
turn/completed的turn.error里(status: "failed"); codexErrorInfo常见值:contextWindowExceeded(上下文超长,触发压缩而非崩溃)、usageLimitExceeded(额度耗尽)、serverOverloaded、responseStreamDisconnected/httpConnectionFailed(带 HTTP 状态码)、cyberPolicy/misalignmentPolicyViolation(安全策略拦截)、activeTurnNotSteerable(插话目标是审查/压缩轮次)等。
A.12 最小客户端骨架(伪代码)
proc = spawn(["codex", "app-server"]) # 默认 stdio,JSONL
next_id = 0
pending = {} # id -> Future
def send(method, params=None, notify=False):
msg = {"method": method, **({"params": params} if params else {})}
if not notify:
msg["id"] = (next_id := next_id + 1)
proc.stdin.write(json.dumps(msg) + "\n")
def on_message(line):
msg = json.loads(line)
if "method" in msg and "id" in msg:
on_server_request(msg) # 反向请求:必须响应
elif "method" in msg:
on_notification(msg["method"], msg.get("params"))
elif "id" in pending:
pending.pop(msg["id"]).resolve(msg.get("result", msg.get("error")))
def on_notification(method, p):
if method == "turn/completed":
render_turn(p["turn"]) # 权威终态
elif method == "item/completed":
upsert_item(p["item"]) # 权威 item
elif method == "item/agentMessage/delta":
append_delta(p["itemId"], p["delta"]) # 临时渲染
elif method == "item/commandExecution/requestApproval":
decision = show_approval_dialog(p) # 弹框
send_response(p["id"], {"decision": decision})
# ...其余通知按 A.8 分发
# 启动三步
send("initialize", {"clientInfo": {"name": "my_client", "version": "0.1"}})
wait_response()
send("initialized", notify=True)
thread = send("thread/start", {"cwd": "/path/to/project"})["thread"]
send("turn/start", {"threadId": thread["id"],
"input": [{"type": "text", "text": "你好"}]})
对接 checklist:
- 按行/按 frame 切分消息,响应靠
id配对; - 严格走完 initialize → initialized 握手再发其他请求;
- 维护 thread/turn/item 三级 UI 模型,item/completed 与 turn/completed 为准,delta 只做临时态;
- 实现全部反向请求的应答(哪怕不支持也回 decline/cancel);
-
willRetry: true的 error 只提示、不改状态; - 用
optOutNotificationMethods关掉不需要的通知控流量; - 断线重连后用
thread/resume+thread/read(或thread/items/list)重新对齐状态; - 字段以随版本发布的 JSON Schema / TypeScript 类型为最终准绳(实验方法会演进,稳定方法保持兼容)。
A.13 与第二章正文的对照表
| 第二章的概念 | app-server 线上的样子 |
|---|---|
| Op(前端 → 内核) | 客户端请求:turn/start、turn/steer、turn/interrupt…… |
| Event(内核 → 前端) | 服务端通知:turn/*、item/*…… |
| “确认即回”(2.3) | turn/start 立即返回 { turn: { status: "inProgress" } },后续走通知 |
| “问与答”反向请求(2.3) | 服务端请求:*/requestApproval、requestUserInput、elicitation…… |
| item 权威 / delta 易失(2.4.5、2.8) | item/completed 重建状态;*/delta 只做打字机;重连不补发 |
| 错误是事件(2.4.4) | error 通知(willRetry 区分重试中 vs 终态) |
| 中断即翻篇(第一章 1.5) | turn/interrupt → turn/completed(status: "interrupted"),后台终端不受影响 |
| 三代事件并存(2.6) | 稳定 item/* 通知 + 内部 rawResponse* 透传;废弃方法保留但标注 deprecated |
| 实验门控(2.7) | capabilities.experimentalApi + [EXP] 标记 |