Codex 协议参考

这一页列出 Codex app-server 适配器在线上读写的字段,以及它们映射成 ChatKit 的什么。怎么接入见 Codex;本页没写的字段以 Codex app-server 文档为准。

传输

项说明
连接一条 WebSocket 上的双向 JSON-RPC,报文省略 jsonrpc。地址由 createCodexConnection({ url }) 给,每次建连时取,可以是异步函数
握手发 initialize(clientInfo 缺省 { name: 'chatkit', version: '0' },capabilities: { experimentalApi: true }),收到结果后发通知 initialized,之后才发其它请求
订阅连接记下当前 WebSocket 上已加载的 thread:经 connection.request 调 thread/start、thread/resume、thread/fork 成功后按结果的 thread.id 记为已加载;发起一轮、停止、插话、listen 前,未加载的先发 thread/resume
分发通知与服务端请求按 params.threadId 分给这个 thread 的监听者;没有 threadId 的不转交
认定一轮监听在 turn/start 之前接好。后端 turnId 取 turn/start 结果的 turn.id,或 item/started 里 userMessage 条目 clientId === runId 时的 turnId(可能先到);认定前收到的内容先缓冲
过滤认定后只放出 params.turnId 或 params.turn.id 等于它的通知与请求,以及本轮接过的请求的处理结果;每项的 eventId 是 {turnId}:{序号};读到 turn/completed 结束
断线等待中的请求以 CodexConnectionClosedError 失败,待回答的服务端请求与已加载 thread 清空,正在读的轮次收到错误。有 listen 监听时自动重连(见下文「listen 断线重连」),否则下一次调用时重新建连、握手、thread/resume
核验版本与稳定性

字段按 codex-cli 0.162.1 的 codex app-server generate-json-schema 与实测事件核对。app-server 协议仍在演进,追问依赖 experimentalApi;升级 Codex 后以包里的 src/wire.types.ts 为准重新核对。

发出的请求

transformInput 收到的 defaultInput 是 { input },必须返回完整请求体;其余字段原样带进 turn/start。

turn/start

字段含义来源transformInput 可改
threadId会话sessionId,连接补上否,连接覆盖
input用户输入正文与引用合并成 { type: 'text', text, text_elements: [] };图片 { type: 'image', url }(内联转 data URL,或 http 地址)按原位置放是
clientUserMessageId用户消息条目的 clientIdChatKit 的 runId否,连接覆盖
其它model、effort、approvalPolicy、collaborationMode 等transformInput 加是

读结果的 turn.id。非图片附件、resource 来源的附件在编码时抛错(cause.code: 'UNSUPPORTED_ATTACHMENT')。

其它请求

方法参数来源读结果
turn/steerthreadId、expectedTurnId、input、clientUserMessageId后端 turnId;input 取 transformInput(capability: 'steer')返回的 input,其余字段不发;clientUserMessageId 是随机 UUID不读
turn/interruptthreadId、turnId后端 turnId;还不知道时抛错、不发请求不读
thread/resumethreadId、excludeTurns: true连接在使用未加载的 thread 前发thread.id
thread/turns/listthreadId、limit(historyPageSize,缺省 20)、sortDirection: 'desc'、itemsView: 'full'、cursor(有时)历史分页data、nextCursor
回复服务端请求{ id, result }connection.respond,见审批与追问—
拒绝服务端请求{ id, error: { code: -32601, message } }不交给协议的服务端请求,收到即回—

其它方法(模型列表、线程改名、归档、fork…)用 connection.request(method, params) 自行调用,后端返回错误时抛 CodexRpcError。

读取的事件

通知读取的字段映射成 ChatKit 的什么
turn/startedturn.id(过滤用)阶段 sampling
item/started、item/completedturnId、item按下表建片段,或以条目内容整个替换(保留交互请求与阶段性输出);回答消息 assistant:{runId} 整条更新;工具为执行中时阶段 tool
同上,item.type 为 userMessageclientId、content只有 item/started 且 clientId !== runId(插话)时出用户消息 user:{clientId ?? id};本地发起的那条由 ChatKit 投影
item/agentMessage/deltaitemId、delta文本片段 item:{itemId} 的增量;片段还没有时新建
item/reasoning/summaryTextDeltaitemId、delta、summaryIndex思考片段 item:{itemId} 的增量;段号变大时先补空行
item/reasoning/textDeltaitemId、delta、contentIndex同上;摘要优先:同一条目先到的是哪种增量就只拼哪种
item/commandExecution/outputDelta、item/fileChange/outputDeltaitemId、delta追加到工具片段 tool:{itemId} 的 partialResult
serverRequest/resolvedthreadId、requestId交互请求标为已回答,见审批与追问
turn/completedturn.status、turn.error(不读 items)先收尾(仍在等待的交互标 cancelled;没正常完成时执行中的工具标 cancelled),再给终态,见结束与错误

turn/completed 的 items 不全(实测只带最后一条回答),回答一律从 item/started / item/completed 与增量拼。

条目类型与片段:

item.type片段工具名 toolNameargs结果与其它
agentMessage文本 item:{id}——text
reasoning思考 item:{id}——summary 各段以空行连接;没有摘要时用 content
commandExecution工具 tool:{id}shellcommand(commandActions[0].command,没有时 command)、cwd非执行中时 result 为 aggregatedOutput;failed 带错误 CODEX_COMMAND_FAILED(含 exitCode)
fileChange工具 tool:{id}apply_patchpath(第一个文件)、changes[].{path, kind}result 为各 diff 以换行连接
mcpToolCall工具 tool:{id}tool;标题 {server} · {tool}arguments(对象或 JSON 字符串)result.structuredContent,没有时 result.content;error.message → CODEX_MCP_ERROR
dynamicToolCall工具 tool:{id}toolargumentscontentItems
webSearch工具 tool:{id}web_searchquery条目没有状态字段:item/started 时执行中,item/completed(及历史)时完成;results 是数组时作为结果

工具状态:inProgress → running,completed → completed,failed → failed,declined → cancelled。片段按条目第一次出现的顺序排,一轮只有一条回答消息。

没有内置处理的事件

下面这些上游内容适配器不转成聊天内容,接入方可以自己取:

途径收到什么
transformOutput本轮放出的每一项 CodexFeedItem(notification、request、request-resolved、request-answering、request-answer-failed),包括不产生默认内容的;返回 OutputChange[],undefined 保留默认内容
connection.listen(sessionId, listener)这个 thread 的全部 CodexRawItem,属于某一轮的与不属于任何一轮的都有;适合驱动插槽

刷新加载历史时,适配器把历史回放成与实时同形的通知逐条交给 transformOutput(context.source 为 history,带这一轮的 runId 与 messageId):每个条目一条 item/completed,结束的轮次一条 turn/completed。item/started、增量、服务端请求和其它通知只在实时里有。业务内容从回放里有的通知生成,刷新后就能还原。

属于某一轮的通知

参数带 turnId,本轮的 transformOutput 与 listen 都能收到。

通知含义
error本轮出错,willRetry 表示是否重试;最终结果看 turn/completed
turn/plan/updated、item/plan/delta计划步骤、计划条目的增量
turn/diff/updated本轮累计的 diff
thread/tokenUsage/updated用量
thread/goal/updated、thread/compacted目标更新、上下文压缩
hook/started、hook/completed钩子执行
item/reasoning/summaryPartAdded思考摘要新起一段(段落已按 summaryIndex 处理)
item/commandExecution/terminalInteraction、item/fileChange/patchUpdated、item/mcpToolCall/progress命令的终端输入、补丁更新、MCP 工具进度
item/autoApprovalReview/started、item/autoApprovalReview/completed、autoApprovalReview/strictReviewRequired自动审批复核
model/rerouted、model/verification、model/safetyBuffering/updated、turn/moderationMetadata模型改道、校验、安全缓冲、审核信息
modelProvider/authRecoveryStarted、modelProvider/authRecoveryCompleted模型服务鉴权恢复
item/started / item/completed 的其它条目类型plan、hookPrompt、functionCallOutput、collabAgentToolCall、subAgentActivity、imageView、sleep、imageGeneration、enteredReviewMode、exitedReviewMode、contextCompaction

不属于某一轮的通知

参数只带 threadId,用 listen 拿。

通知含义
thread/name/updated、thread/settings/updated、thread/status/changed标题、设置、运行状态
thread/archived、thread/unarchived、thread/deleted、thread/closed、thread/reverted线程生命周期
thread/goal/cleared、thread/queue/changed、thread/prediction/updated、thread/attachment/updated、thread/project/updated目标、排队、预测、附件、项目
thread/environment/connected、thread/environment/disconnected执行环境连接
mcpServer/oauthLogin/completed、mcpServer/startupStatus/updatedMCP 服务登录、启动状态
warning、guardianWarning警告
thread/realtime/started、thread/realtime/itemAdded、thread/realtime/item/started、thread/realtime/item/transcript/delta、thread/realtime/item/completed、thread/realtime/transcript/delta、thread/realtime/transcript/done、thread/realtime/outputAudio/delta、thread/realtime/sdp、thread/realtime/error、thread/realtime/closed实时语音会话

不带 threadId 的通知

连接按 thread 分发,这些通知不转交给 listen;需要时由接入方另开一条 app-server 连接处理。

通知含义
account/rateLimits/updated、account/updated、account/login/completed、account/gatewayOAuth/changed限流、账号、登录
thread/started、skills/changed、project/changed、app/list/updated、remoteControl/status/changed全局列表与状态
command/exec/outputDelta、process/outputDelta、process/exited、fs/changed、fuzzyFileSearch/sessionUpdated、fuzzyFileSearch/sessionCompleted独立命令、进程、文件监听、文件搜索
mcpServer/event/stream/notification、externalAgentConfig/import/progress、externalAgentConfig/import/completedMCP 事件流、外部配置导入
deprecationNotice、configWarning、windows/worldWritableWarning、windowsSandbox/setupCompleted提示与平台相关

服务端请求

只有 item/commandExecution/requestApproval、item/fileChange/requestApproval、item/tool/requestUserInput(且带 threadId)交给协议。其余服务端请求连接收到即回 -32601,不让这一轮干等;它们不进 listen 与 transformOutput,连接的 onError 收到 kind: 'request' 与 { method }。

请求含义是否属于某一轮
item/tool/call动态工具调用,由客户端执行是
mcpServer/elicitation/requestMCP 表单是
item/permissions/requestApproval权限申请是
currentTime/read读取客户端时间否
account/chatgptAuthTokens/refresh、attestation/generate凭据刷新、证明生成否
applyPatchApproval、execCommandApproval旧版审批(按 conversationId)否

listen 断线重连

连接断开(或这个会话的订阅单独结束)时,有 listen 监听的会话:

  1. 监听先收到 { kind: 'connection', status: 'disconnected' };
  2. 连接按退避自动重连并重新订阅(重新握手并 thread/resume):第一次等 reconnect.minDelayMs(缺省 1000),之后翻倍,最多等 reconnect.maxDelayMs(缺省 30000);
  3. 订阅恢复后收到 { kind: 'connection', status: 'reconnected' }。断线期间的事件不会补发,需要时自行补拉状态(如 thread/read)。

每次重新订阅失败经 onError('listen', error) 报告,之后继续重试。取消全部监听或调用 close() 后停止;连接选项 reconnect: false 关闭自动重连(仍会收到 disconnected)。正在进行的一轮不续接,仍由会话层经历史确认终态。

一轮都没跑过的 thread 不落盘,连接断开后 thread/resume 会失败(no rollout found),重连会一直重试并报告错误。

示例

const off = connection.listen(sessionId, item => {
  if (item.kind !== 'notification') return;
  if (item.method === 'thread/name/updated') {
    const { threadName } = item.params as { threadName: string | null };
    setTitle(threadName ?? '');
  }
});
const adapter = createCodexAdapter({
  connection,
  transformOutput: (item, context) => {
    if (!context.messageId) return undefined;
    if (item.kind !== 'notification' || item.method !== 'turn/plan/updated') return undefined;
    const { plan } = item.params as { plan: unknown[] };
    return [
      ...context.defaultChanges,
      {
        type: 'set-part',
        messageId: context.messageId,
        part: { id: 'codex-plan', type: 'data', kind: 'codex.plan', data: plan },
      },
    ];
  },
});

turn/plan/updated 不在历史里,这张计划卡片刷新后不会还原;要还原就从 item/completed 里能拿到的内容生成。

context.messageId 是本轮的回答消息,回答还没有内容时为空。

审批与追问

服务端请求读取的字段挂在哪
item/commandExecution/requestApprovalthreadId、turnId、itemId、command、reason、availableDecisions工具片段 tool:{itemId}(还没有时新建 shell,执行中)
item/fileChange/requestApprovalthreadId、turnId、itemId、reason工具片段 tool:{itemId}(还没有时新建 apply_patch,执行中)
item/tool/requestUserInputthreadId、turnId、itemId、questions[].{id, header, question, options, isOther, isSecret}合成工具片段 tool:{itemId},toolName: 'request_user_input',args: { questions }(原样)

收到请求后阶段为 awaiting-external。可选回答只取 availableDecisions 里属于 accept、acceptForSession、decline、cancel 的字符串,带执行策略修订的对象形式不提供;一个都没有时给全部四个。

交互请求在工具片段的 metadata['chatkit.interaction'],用 getCodexInteraction(part) 读:

字段审批 kind: 'approval'追问 kind: 'question'
requestIdJSON-RPC 请求 id 转成字符串;已回答、已失效后去掉同左
status见下见下
callIditemIditemId
toolNamecommandExecution 或 fileChange—
reasonreason(有时)—
commandcommand(命令审批时有)—
decisions可以给的回答—
decision本客户端给的回答(已回答时)—
questions—{ id, header?, question, options?, allowOther?, secret? }[],allowOther 来自 isOther,secret 来自 isSecret
answers—本客户端给的回答 { id, selected, custom? }[](已回答时)
status何时
pending收到请求;或 respond 建连失败后退回
answering调用了 connection.respond,等后端确认
answered收到 serverRequest/resolved(threadId、requestId);追问的工具片段同时标完成,result 为 answers(别的客户端回答时为 null)。没有其它等待中的请求时阶段回到 tool
cancelledturn/completed 时仍在 pending 或 answering

connection.respond({ sessionId, requestId, response }) 按原请求 id 发回:

请求response发出的 JSON-RPC
审批'accept'、'acceptForSession'、'decline'、'cancel' 之一{ "id": <原 id>, "result": { "decision": "accept" } }
追问{ answers: [{ id, selected, custom? }] }{ "id": <原 id>, "result": { "answers": { "<id>": { "answers": [...selected, custom] } } } },没有 custom 时只有 selected

请求已处理、或连接断开后(待回答请求已清空)再调用会抛错;respond 发出即返回,不等 serverRequest/resolved。

历史

thread/turns/list 返回 data(轮次,从新到旧)与 nextCursor;连接整理成 { turns, nextCursor },解码时反转成时间顺序。每个轮次读 id、status、items、error、startedAt、completedAt(Unix 秒)。

产物规则
runId这一轮第一条带 clientId 的 userMessage 的 clientId(本地发起时就是 runId);都没有时 codex-turn:{turnId}
用户消息每条 userMessage 一条,user:{clientId ?? id},排在回答之前,createdAt 为 startedAt × 1000;片段经 chatInputParts 生成,图片附件编号 {messageId}:{序号}
回答消息其余条目按实时同一套规则拼成 assistant:{runId};已结束的轮次同样收尾
轮次结果只给已结束的轮次:origin: 'remote'、状态按结束与错误映射、createdAt、finishedAt(不早于开始时才给)、input 取第一条用户消息;进行中的轮次只给消息,不给 activeRun
分页hasMore 为 nextCursor 非空,cursor 即 nextCursor

Codex 历史不保留:审批记录(刷新后工具只有最终状态,没有交互请求)、追问(卡片不再出现)、被插话打断的那段回答、停止时已输出的部分回答。插话的用户消息实时显示在回答之后,刷新后排在回答之前。

编号

ChatKit 编号来源
sessionIdCodex 的 threadId,会话由接入方 thread/start 创建
runId本地生成,作为 clientUserMessageId 发出;历史见上
后端轮次turnId,连接按 runId 保存,停止、插话用
用户消息user:{clientId};本地发起 user:{runId},插话 user:{随机 UUID},无 clientId 时 user:{itemId}
回答消息assistant:{runId}
文本 / 思考片段item:{itemId}
工具片段tool:{itemId},toolCallId 为 itemId
交互 requestIdJSON-RPC 服务端请求 id 转成字符串
eventId{turnId}:{序号}

结束与错误

turn.statusChatKit 终态
completedcompleted
interruptedinterrupted(原因 protocol)
其它failed,错误 { code: 'CODEX_TURN_FAILED', message: turn.error.message 或「Agent 没能完成这一轮」, cause: turn.error }
错误何时
CodexRpcError请求收到 JSON-RPC 错误响应;带 code、data,消息取后端的 message
CodexConnectionClosedError连接没能建立、断开或已 close();消息不带地址
Error停止、插话时还不知道后端 turnId;respond 的请求已不能回答;历史缺少 turns 数组
附件错误非图片或 resource 来源,cause.code: 'UNSUPPORTED_ATTACHMENT'
工具片段错误CODEX_COMMAND_FAILED、CODEX_MCP_ERROR,只影响片段,不影响这一轮
onError(kind, error, extra)连接内部问题:frame(报文解析失败)、listener(监听抛错)、listen(订阅失败)、request(拒绝的服务端请求)

断线时这一轮由 Core 标记连接中断,不认定结束;适配器不支持续接,由会话层经历史确认终态。runId → turnId 留到读到 turn/completed(或 turn/start 失败)才删,所以断线后仍能停止、插话:连接先在新 WebSocket 上 thread/resume,再发 turn/interrupt / turn/steer。

示例

一轮文本回答(一行一条报文,→ 为发出,← 为收到):

→ {"id":3,"method":"turn/start","params":{"threadId":"thread-1","input":[{"type":"text","text":"你好","text_elements":[]}],"clientUserMessageId":"run-1"}}
← {"id":3,"result":{"turn":{"id":"turn-1","status":"inProgress","items":[]}}}
← {"method":"turn/started","params":{"threadId":"thread-1","turn":{"id":"turn-1","status":"inProgress","items":[]}}}
← {"method":"item/started","params":{"threadId":"thread-1","turnId":"turn-1","item":{"type":"agentMessage","id":"msg-1","text":""}}}
← {"method":"item/agentMessage/delta","params":{"threadId":"thread-1","turnId":"turn-1","itemId":"msg-1","delta":"这是"}}
← {"method":"item/completed","params":{"threadId":"thread-1","turnId":"turn-1","item":{"type":"agentMessage","id":"msg-1","text":"这是回答"}}}
← {"method":"turn/completed","params":{"threadId":"thread-1","turn":{"id":"turn-1","status":"completed","items":[]}}}

得到消息 assistant:run-1,文本片段 item:msg-1,终态 completed。

命令审批:

← {"id":101,"method":"item/commandExecution/requestApproval","params":{"threadId":"thread-1","turnId":"turn-1","itemId":"call_1","command":"/bin/bash -lc 'printf ok'","reason":"需要执行命令","availableDecisions":["accept","acceptForSession",{"acceptWithExecpolicyAmendment":{}},"cancel"]}}
→ {"id":101,"result":{"decision":"accept"}}
← {"method":"serverRequest/resolved","params":{"threadId":"thread-1","requestId":101}}

工具片段 tool:call_1 上的交互请求依次为 pending(decisions: ['accept', 'acceptForSession', 'cancel'])→ answering → answered(decision: 'accept')。