从 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 适配器提供 exec 和 app_server 两个后端。
exec 是默认后端。每轮交互启动一次 codex exec --json;已有会话时使用 codex exec resume <thread_id>。Prompt 从子进程 stdin 传入,stdout 按 JSONL 逐行读取,本轮结束后子进程退出。
app_server 后端启动一个持续运行的进程:
codex app-server客户端通过 JSON-RPC 完成初始化、创建或恢复线程,并启动 turn:
initializeinitializedthread/start 或 thread/resumeturn/start配置示例:
[projects.agent]type = "codex"
[projects.agent.options]backend = "app_server"work_dir = "/path/to/project"两个后端均转换为统一事件。Engine 根据事件类型发送文本、更新流式卡片,或者向用户发起权限确认。
stdin 和 stdout:进程的标准流
printf 与 grep 之间的标准流连接为:
printf 'apple\nbanana\n' | grep apple数据流向为:
printf 的 stdout ─────> grep 的 stdingrep 的 stdout ─────> 终端stdin 是进程的标准输入,stdout 是进程的标准输出。二者对应操作系统提供的文件描述符,传输内容的本质是字节流。输入可以来自终端、文件、管道或父进程创建的 pipe。
标准流不定义“消息”这个概念。接收端只能读取连续字节,消息边界必须由上层协议约定。例如:
第一条消息第二条消息常见的消息边界方案包括:
- 每行一条消息,例如 JSONL;
- 消息前面带长度字段;
- 使用固定大小的帧或自己定义分隔符。
cc-connect 的两个 Codex 适配器都采用“每行一个 JSON 对象”的消息边界。app-server 的读取循环按换行读取数据,再根据 JSON 中是否存在 id 和 method,将消息识别为响应、服务端请求或通知。
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.1Host: agent.example.comContent-Type: application/jsonAuthorization: Bearer <token>
{"threadId":"thread-id","input":"帮我检查代码"}服务器会返回状态码和响应头:
HTTP/1.1 200 OKContent-Type: application/json
{"turnId":"turn-id"}HTTP 的消息边界由协议规则处理,例如 Content-Length、分块传输和 HTTP/2 帧。除此之外,HTTP 还定义了 URL 路由、状态码、缓存、认证、代理和连接管理等网络服务语义。
stdin/stdout 与 HTTP 的边界差异如下:
| 对比项 | stdin/stdout | HTTP |
|---|---|---|
| 主要对象 | 一个进程和它的父进程 | 客户端和服务端 |
| 默认范围 | 本机进程间 | 可以跨机器和网络 |
| 数据边界 | 需要自行约定 | 由 HTTP 消息格式处理 |
| 协议语义 | 通道本身没有方法、状态码 | 有方法、URL、Header、状态码 |
| 多客户端 | 通常需要自己管理多个子进程 | 服务端天然面向多个连接 |
| 调试方式 | 查看进程、管道和日志 | curl、浏览器、代理和网关 |
| 运维成本 | 进程生命周期和退出码 | 端口、认证、TLS、超时、重试和监控 |
| 适合场景 | 本地 CLI、脚本、子进程 Agent | 远程 API、微服务、公共服务 |
HTTP 与流式传输属于不同维度。HTTP 请求可以一次返回完整结果,也可以通过 SSE、分块响应或 WebSocket 持续发送事件。流式传输并非 HTTP 专属能力,stdio 同样可以逐行发送 JSON 事件;HTTP 的区别在于,它同时提供了网络服务所需的连接、路由和治理能力。
本地 Agent 适合使用 stdio 的原因
Codex 在该架构中是本机 CLI,cc-connect 与它运行在同一台机器上。使用 stdio 的主要原因是:
- 不需要额外监听端口,也不需要配置服务发现。
- 子进程的启动、退出和工作目录都由调用方管理。
- 输入输出可以直接接到 Unix 管道,延迟和额外开销较低。
- 不把本地 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.go:exec后端的 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 文档:
initialize、thread/start、thread/resume和turn/start的协议说明。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
