从 cc-connect 看 Codex CLI 的交互模式:stdin、stdio 与 HTTP

2461 字
12 分钟
从 cc-connect 看 Codex CLI 的交互模式:stdin、stdio 与 HTTP

cc-connect 将聊天平台接收到的消息转发给本机 Codex CLI,并将 Codex 返回的文本、工具调用、工具结果和权限请求转换为平台可以消费的事件。它不通过 OpenAI REST API 直接完成这条交互链路,而是把 Codex CLI 作为子进程运行,通过进程管道交换数据。

cc-connect 的 Agent 适配层#

cc-connect 将 Codex 封装为统一的 Agent 会话。上层 Engine 不需要区分底层使用一次性 CLI 还是长期运行的 app-server,只依赖会话接口:

StartSession(sessionID)
Send(prompt)
Events()
RespondPermission(...)

一次消息处理的调用链为:

Codex 服务codex CLIagent/codexcore.Engine飞书/Telegram 等平台用户Codex 服务codex CLIagent/codexcore.Engine飞书/Telegram 等平台用户alt[默认 exec 后端][app_server 后端]发送消息MessageStartSession(sessionID)启动 codex 进程Send(prompt)codex exec --jsonJSONL 事件codex app-serverinitialize / thread/startSend(prompt)JSON-RPC turn/startJSON-RPC 通知和事件请求模型服务模型响应文本、工具调用、工具结果EventText / EventToolUse / EventResult回复或流式卡片展示结果
Codex 服务codex CLIagent/codexcore.Engine飞书/Telegram 等平台用户Codex 服务codex CLIagent/codexcore.Engine飞书/Telegram 等平台用户alt[默认 exec 后端][app_server 后端]发送消息MessageStartSession(sessionID)启动 codex 进程Send(prompt)codex exec --jsonJSONL 事件codex app-serverinitialize / thread/startSend(prompt)JSON-RPC turn/startJSON-RPC 通知和事件请求模型服务模型响应文本、工具调用、工具结果EventText / EventToolUse / EventResult回复或流式卡片展示结果

Codex 适配器提供 execapp_server 两个后端。

exec 是默认后端。每轮交互启动一次 codex exec --json;已有会话时使用 codex exec resume <thread_id>。Prompt 从子进程 stdin 传入,stdout 按 JSONL 逐行读取,本轮结束后子进程退出。

app_server 后端启动一个持续运行的进程:

Terminal window
codex app-server

客户端通过 JSON-RPC 完成初始化、创建或恢复线程,并启动 turn:

initialize
initialized
thread/start 或 thread/resume
turn/start

配置示例:

[projects.agent]
type = "codex"
[projects.agent.options]
backend = "app_server"
work_dir = "/path/to/project"

两个后端均转换为统一事件。Engine 根据事件类型发送文本、更新流式卡片,或者向用户发起权限确认。

stdin 和 stdout:进程的标准流#

printfgrep 之间的标准流连接为:

Terminal window
printf 'apple\nbanana\n' | grep apple

数据流向为:

printf 的 stdout ─────> grep 的 stdin
grep 的 stdout ─────> 终端

stdin 是进程的标准输入,stdout 是进程的标准输出。二者对应操作系统提供的文件描述符,传输内容的本质是字节流。输入可以来自终端、文件、管道或父进程创建的 pipe。

标准流不定义“消息”这个概念。接收端只能读取连续字节,消息边界必须由上层协议约定。例如:

第一条消息
第二条消息

常见的消息边界方案包括:

  • 每行一条消息,例如 JSONL;
  • 消息前面带长度字段;
  • 使用固定大小的帧或自己定义分隔符。

cc-connect 的两个 Codex 适配器都采用“每行一个 JSON 对象”的消息边界。app-server 的读取循环按换行读取数据,再根据 JSON 中是否存在 idmethod,将消息识别为响应、服务端请求或通知。

stdio:stdin、stdout 和 stderr#

stdio 不是独立于 stdin/stdout 的第四种传输方式,而是对进程标准流的统称:

stdin 标准输入
stdout 标准输出
stderr 标准错误

三个标准流的职责分别是:

  • stdin 是一个方向,表示“写给进程的输入”;
  • stdout 是另一个方向,表示“从进程读出的正常输出”;
  • stderr 通常用于日志和诊断;
  • stdio 通常指通过这几个标准流和进程通信。

cc-connect 启动 app-server 时,为子进程分别建立三个管道:

cmd := exec.CommandContext(ctx, "codex", "app-server")
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
cmd.Start()

适配器将 JSON-RPC 请求写入 stdin,从 stdout 读取协议事件,并单独消费 stderr。协议数据和日志必须分流:如果日志写入 stdout,JSON 解析器就无法可靠地区分日志行和协议消息。

两个后端对 stdin 的使用方式不同:exec 后端将 stdin 用作当前 turn 的 Prompt 输入;app_server 后端则在整个子进程生命周期内复用 stdin,连续承载多次请求。

JSON-RPC:建立在 stdio 之上的消息协议#

stdin/stdout 只提供字节传输;app-server 在此基础上使用 JSON-RPC 风格的消息定义请求、响应和事件结构。典型请求如下:

{
"jsonrpc": "2.0",
"id": 12,
"method": "turn/start",
"params": {
"threadId": "thread-id",
"input": [
{"type": "text", "text": "帮我检查代码"}
]
}
}

换行符作为消息分隔符,使一个 JSON 对象形成一条 JSONL 消息:

JSON 对象 + '\n' = 一条可读取的协议消息

服务端响应携带与请求相同的 id

{
"jsonrpc": "2.0",
"id": 12,
"result": {
"turn": {"id": "turn-id"}
}
}

通知不包含 id,例如:

{
"jsonrpc": "2.0",
"method": "item/agentMessage/delta",
"params": {"delta": "正在检查..."}
}

服务端也可以向客户端发起请求。Codex 需要执行命令或修改文件时发送权限请求:

{
"jsonrpc": "2.0",
"id": 31,
"method": "item/commandExecution/requestApproval",
"params": {"command": "go test ./..."}
}

权限请求属于服务端请求而非普通通知:它包含 id,客户端必须使用同一个 id 回复。cc-connect 将其转换成 EventPermissionRequest,由聊天平台收集用户的允许或拒绝决定,再将结果写回 app-server 的 stdin。

一条 app-server stdio 连接同时承载三类消息:

客户端请求 initialize、thread/start、turn/start
服务端响应 携带相同 id 的 result/error
服务端通知/请求 流式文本、工具事件、权限请求

三类消息可以交错到达,客户端不能采用简单的同步“发送一条、读取一条”模型。当前实现维护待处理请求表,根据 id 将响应分发给对应的等待者;独立的读取循环持续处理通知和服务端请求。写入 stdin 需要串行化,否则多个 goroutine 同时写入时可能造成 JSON 内容交错。

HTTP:面向网络服务的通信协议#

HTTP 可以承载 JSON,但其协议语义不止于 JSON 传输。一个 HTTP 请求至少包含方法、路径、Header 和 Body:

POST /turns HTTP/1.1
Host: agent.example.com
Content-Type: application/json
Authorization: Bearer <token>
{"threadId":"thread-id","input":"帮我检查代码"}

服务器会返回状态码和响应头:

HTTP/1.1 200 OK
Content-Type: application/json
{"turnId":"turn-id"}

HTTP 的消息边界由协议规则处理,例如 Content-Length、分块传输和 HTTP/2 帧。除此之外,HTTP 还定义了 URL 路由、状态码、缓存、认证、代理和连接管理等网络服务语义。

stdin/stdout 与 HTTP 的边界差异如下:

对比项stdin/stdoutHTTP
主要对象一个进程和它的父进程客户端和服务端
默认范围本机进程间可以跨机器和网络
数据边界需要自行约定由 HTTP 消息格式处理
协议语义通道本身没有方法、状态码有方法、URL、Header、状态码
多客户端通常需要自己管理多个子进程服务端天然面向多个连接
调试方式查看进程、管道和日志curl、浏览器、代理和网关
运维成本进程生命周期和退出码端口、认证、TLS、超时、重试和监控
适合场景本地 CLI、脚本、子进程 Agent远程 API、微服务、公共服务

HTTP 与流式传输属于不同维度。HTTP 请求可以一次返回完整结果,也可以通过 SSE、分块响应或 WebSocket 持续发送事件。流式传输并非 HTTP 专属能力,stdio 同样可以逐行发送 JSON 事件;HTTP 的区别在于,它同时提供了网络服务所需的连接、路由和治理能力。

本地 Agent 适合使用 stdio 的原因#

Codex 在该架构中是本机 CLI,cc-connect 与它运行在同一台机器上。使用 stdio 的主要原因是:

  1. 不需要额外监听端口,也不需要配置服务发现。
  2. 子进程的启动、退出和工作目录都由调用方管理。
  3. 输入输出可以直接接到 Unix 管道,延迟和额外开销较低。
  4. 不把本地 Agent 暴露成一个网络服务,边界更简单。

代价是调用方必须自行处理进程崩溃、超时、EOF、半条消息、JSON 解析错误、请求响应关联,以及 stdout/stderr 分流。如果需要让多台机器或多个独立客户端访问同一个 Agent,stdio 就不再适合作为服务边界,应考虑 HTTP、WebSocket、gRPC 或消息队列。

app-server 的作用是为一次性 CLI 增加长期进程和会话生命周期。外部程序启动一次进程后,可以复用线程,连续发送多个 turn/start,并同时接收文本增量、工具事件和权限请求。

通信层次#

应用行为:创建线程、发送 turn、请求权限
应用协议:JSON-RPC / JSONL
传输通道:本地 stdin/stdout(stdio)
进程边界:父进程 ← pipe → Codex CLI 子进程

Agent 部署为网络服务时,通信层次为:

应用行为:创建线程、发送 turn、请求权限
应用协议:HTTP API,或 HTTP 上的 SSE/WebSocket
网络传输:TCP,生产环境通常再加 TLS
服务边界:多个客户端 ← 网络 → Agent 服务

“通过 stdio 通信”不等于“没有协议”,它只说明最底层使用了进程标准流;上层仍然可以承载 JSON-RPC、MCP 或其他完整协议。“通过 HTTP 调用”也不等于业务层的会话、权限和事件模型已经定义完成,这些内容仍然属于应用协议。

cc-connect 中,适配层负责在进程管道上编码并发送协议消息,并将 Codex 返回的事件转换为统一的 Agent 事件。适配器的关键职责包括消息边界处理、并发响应分发、流式事件消费和权限结果回传。

参考位置#

  • cc-connect/agent/codex/codex.go:后端选择与会话创建。
  • cc-connect/agent/codex/session.goexec 后端的 stdin 输入和 JSONL 输出。
  • cc-connect/agent/codex/appserver_session.go:app-server 的 stdio 管道、JSON-RPC 请求和事件读取。
  • cc-connect/core/engine.go:会话恢复、发送消息和事件消费。
  • cc-connect/core/message.go:统一 Agent 事件类型。
  • OpenAI Codex App Server 文档initializethread/startthread/resumeturn/start 的协议说明。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

从 cc-connect 看 Codex CLI 的交互模式:stdin、stdio 与 HTTP
https://blog.sephy.top/posts/codex-cli-interactive-mode-stdin-stdio-http/
作者
虾米
发布于
2026-08-12
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
虾米
coder
分类
标签
站点统计
文章
74
分类
12
标签
85
总字数
102,901
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.15.8
文章许可
CC BY-NC-SA 4.0