附录 A 前端对接协议参考(app-server JSON-RPC)

第二章讲的是协议设计:为什么是两条消息河流、事件为什么分层。本附录是对接手册: 如果你要自己写一个前端(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 /healthzGET /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 消息信封:四种消息

线上只有四种消息,靠 idmethod 字段区分:

// ① 请求(有 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/startthread/resumethread/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:有轮次在跑,附带 activeFlagswaitingOnApproval(等审批)、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 等):inProgresscompleted / 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 的协议化版本):

  1. item 是权威,delta 是加速带。UI 的数据模型以 item/completed 为准重建;delta 只用来拼“正在生成”的临时气泡。断线重连/迟到订阅后,用 thread/items/listturn/completed 里的 items 对齐,delta 漏了不补发;
  2. 所有通知带坐标threadId / turnId / itemId,按坐标归位到对应线程面板;
  3. turn 的终态只看 turn/completedturn/interrupt 的响应只表示“已受理”,轮次真正结束以 turn/completedstatus: "interrupted")为准。

A.6 客户端 → 服务端方法速查

只列对接常用方法;完整字段以随包发布的 JSON Schema / TypeScript 类型为准。

会话与轮次(最常用)

方法 作用 关键 params / result
initialize 握手 params:clientInfo{name,title?,version?}capabilities?;result:userAgentcodexHomeplatformFamilyplatformOs
thread/start 新建线程 params:cwd?model?approvalPolicy?sandbox?/sandboxPolicy?personality? 等;result:{ thread };随后有 thread/started 通知并自动订阅
thread/resume 恢复历史线程 params:threadId(+ 可选策略覆盖);result:{ thread }
thread/fork 分叉线程 params:threadIdlastTurnId?ephemeral?;result:{ thread }(含 forkedFromId
thread/list 线程列表(游标分页) params:cursor?limit?、过滤项;result:{ data, nextCursor }
thread/read 读线程(不恢复) params:threadIdincludeTurns?
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:threadIdinputexpectedTurnId必填)、clientUserMessageId?;result:{ turnId }
turn/interrupt 中断当前轮 params:threadIdturnId;result:{};终态等 turn/completed
review/start 代码审查 params:threadIdtargetdelivery: "inline"/"detached"
thread/compact/start 手动压缩上下文 params:threadId
thread/shellCommand 跑一次性 !命令 params:threadIdcommand
thread/rollback(废弃) 丢弃最近 N 个用户轮次的历史(不动磁盘文件) params:threadIdnumTurns;result:更新后的 thread
thread/revert [EXP] 新版回滚:把历史替换为某个 turn 之前的前缀 params:threadIdbeforeTurnId
thread/inject_items 往历史注入原始 item(不开轮次) params:threadIditems(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/updatedthread/goal/updated / cleared 重命名、长期目标变化
turn/started 轮次真正开始运行
turn/completed 轮次终态:完整 turn 对象(statusitemserror、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/changedaccount/updatedaccount/rateLimits/updatedapp/list/updatedskills/changedmcpServer/startupStatus/updated 各类资源变更广播
rawResponseItem/completedrawResponse/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/completedturn/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 状态码的对象形式): contextWindowExceededsessionBudgetExceededusageLimitExceededserverOverloadedcyberPolicymisalignmentPolicyViolationbadRequestunauthorizedsandboxErrorinternalServerErrorother;对象形式有 httpConnectionFailedresponseStreamConnectionFailedresponseStreamDisconnectedresponseTooManyFailedAttempts(均带 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/completedturn.error 里(status: "failed");
  • codexErrorInfo 常见值:contextWindowExceeded(上下文超长,触发压缩而非崩溃)、usageLimitExceeded(额度耗尽)、serverOverloadedresponseStreamDisconnected / 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/startturn/steerturn/interrupt……
Event(内核 → 前端) 服务端通知:turn/*item/*……
“确认即回”(2.3) turn/start 立即返回 { turn: { status: "inProgress" } },后续走通知
“问与答”反向请求(2.3) 服务端请求:*/requestApprovalrequestUserInputelicitation……
item 权威 / delta 易失(2.4.5、2.8) item/completed 重建状态;*/delta 只做打字机;重连不补发
错误是事件(2.4.4) error 通知(willRetry 区分重试中 vs 终态)
中断即翻篇(第一章 1.5) turn/interruptturn/completedstatus: "interrupted"),后台终端不受影响
三代事件并存(2.6) 稳定 item/* 通知 + 内部 rawResponse* 透传;废弃方法保留但标注 deprecated
实验门控(2.7) capabilities.experimentalApi + [EXP] 标记
分类:Agent Harness标签:#agent #harness #codex