codex exec 与 SDK · 没有界面的用法

codex exec 是一个把 app-server 嵌在自己进程里的无界面客户端:发 thread/start 与 turn/start,把 v2 通知翻译成给人看的输出或一套更简单的 JSONL 事件,遇到审批请求一律拒绝,本轮结束就退出。TypeScript SDK 每一轮都拉起一次 codex exec 读它的 JSONL;Python SDK 则直接经 stdio 连 codex app-server,讲完整的 v2 协议。

作者 David更新于 第 11 篇(共 57 篇)

codex exec 与 SDK · 没有界面的用法

前面几篇的主角是交互式前端背后的 app-server。这一篇看三种不带界面的用法:codex exec 命令(codex-rs/exec,crate 名 codex-exec),以及仓库 sdk/ 目录下的 TypeScript 与 Python 两个 SDK。它们接入 Codex 的方式各不相同。

用户看到的样子

codex exec "任务" 跑完一轮就退出:进度写 stderr,最终回复写 stdout,--json 把 stdout 换成 JSONL 事件流,失败时退出码为 1。参数、默认的沙箱与审批、事件类型与结构化输出见手册非交互模式 codex exec;两个 SDK 的安装与示例见SDK 与 App Server。

图表加载中…

codex exec:一个无界面的 app-server 客户端

codex exec 与 TUI 一样不直接调用内核,而是用 InProcessAppServerClient 在进程里嵌一个 app-server(第 6 篇),客户端名 codex_exec,会话来源 SessionSource::Exec,并且允许用环境变量 CODEX_API_KEY 提供凭据。构建配置时,它先把审批策略固定为 AskForApproval::Never,注释称这是 headless 模式的默认值;只有当最终生效的审批人是自动审查(AutoReview)时,才撤掉这个覆盖、沿用配置里的策略,而加了 --dangerously-bypass-approvals-and-sandbox 时始终保持 Never。

run_exec_session(codex-rs/exec/src/lib.rs:833)的流程是:

  1. 按 --json 选事件处理器:EventProcessorWithJsonOutput 或 EventProcessorWithHumanOutput,两者实现同一个 EventProcessor trait,输入都是 v2 的 ServerNotification。
  2. 新建、恢复或分叉线程:分别发 thread/start、thread/resume、thread/fork。新建的非临时线程会请求 paginated 历史格式,服务端不支持时去掉该参数重试。注释特意说明,直接拿 start、resume 的响应当启动信息,不再等之后推来的 SessionConfigured 事件,因为在进程内路径上那样做会白白多等最多 10 秒。
  3. 发 turn/start,turn_trigger 填 "exec";codex exec review 改发 review/start;codex exec fork 不带提示词时只分叉、不开轮次。
  4. 进入事件循环:Ctrl-C 被转成 turn/interrupt;通知只保留属于这个线程、这一轮的(should_process_notification),交给事件处理器;处理器在 turn/completed 时返回 InitiateShutdown,于是发 thread/unsubscribe、关闭内嵌服务端,出现过不再重试的错误,或本轮以失败、中断结束,就以退出码 1 退出。

没有人可以点“同意”,服务端请求在 handle_server_request 里统一处理:MCP 的 elicitation 回“取消”,其余请求(命令与文件修改的审批、request_user_input、动态工具调用、权限申请等)全部以 -32000 拒绝:

        ServerRequest::CommandExecutionRequestApproval { request_id, params } => {
            reject_server_request(
                client,
                request_id,
                &method,
                format!(
                    "command execution approval is not supported in exec mode for thread `{}`",
                    params.thread_id
                ),
            )
            .await
        }

(codex-rs/exec/src/lib.rs:2014)

结合第 6 篇讲的审批往返:客户端回了错误,app-server 就以 ReviewDecision::denied 把决定交回 core,那一步操作不会执行。

还有一处能看出分层之间的配合。进程内传输在队列满时可能丢掉普通通知,但 turn/completed 保证送达;而且 app-server 目前发出的 turn/completed 里 items 是空的。exec 因此在收尾时补读一次:

async fn maybe_backfill_turn_completed_items(
    thread_ephemeral: bool,
    client: &InProcessAppServerClient,
    request_ids: &mut RequestIdSequencer,
    notification: &mut ServerNotification,
) {
    // In-process delivery may drop non-terminal item notifications under backpressure while still
    // guaranteeing `turn/completed`. Because app-server currently emits that completion with an
    // empty `turn.items`, exec does one last `thread/read` here so human/json output can recover
    // the final message and reconcile any still-running items before shutdown.
    if !should_backfill_turn_completed_items(thread_ephemeral, notification) {
        return;
    }

(codex-rs/exec/src/lib.rs:1655)

临时线程没有落盘的历史可读,所以跳过这一步。

两种输出

给人看的处理器把进度写到 stderr,收尾时在 stderr 报出这一轮用掉的 token 数;stdout 与 stderr 只要有一个不是终端,就把最终回复单独打印到 stdout,因此 codex exec "修复测试" > out.txt 拿到的正好是回复本身。

JSONL 处理器输出的不是 v2 协议,而是 exec_events.rs 定义的另一套更简单、字段为 snake_case 的事件:

/// Top-level JSONL events emitted by codex exec
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
#[serde(tag = "type")]
pub enum ThreadEvent {
    /// Emitted when a new thread is started as the first event.
    #[serde(rename = "thread.started")]
    ThreadStarted(ThreadStartedEvent),

(codex-rs/exec/src/exec_events.rs:8)

顶层事件只有 thread.started、turn.started、turn.completed(带 token 用量)、turn.failed、item.started、item.updated、item.completed 与 error 八种;条目类型也收窄为 agent_message、reasoning、command_execution、file_change、mcp_tool_call、collab_tool_call、web_search、todo_list、error 九种。处理器把 v2 通知逐个翻译过来:条目 id 重新编号为 item_0、item_1……;turn/plan/updated 变成一个 todo_list 条目的开始与更新,本轮结束时再补一个完成事件;turn/completed 按状态变成 turn.completed 或 turn.failed。这层翻译让脚本面对的是一个稳定的小接口,而不是随版本增长的完整协议。

TypeScript SDK:每一轮一个 codex exec

sdk/typescript 不讲 app-server 协议,它包装的是 codex exec:

  async *run(args: CodexExecArgs): AsyncGenerator<string> {
    const commandArgs: string[] = ["exec", "--experimental-json"];
// ...
    if (args.threadId) {
      commandArgs.push("resume", args.threadId);
    }
// ...
    const child = spawn(this.executablePath, commandArgs, {
      env,
      signal: args.signal,
    });
// ...
    child.stdin.write(args.input);
    child.stdin.end();

(sdk/typescript/src/exec.ts:91)

--experimental-json 是 --json 的别名。每次 thread.run() 或 runStreamed() 都拉起一个新进程:提示词写进 stdin,stdout 按行解析成上面那套事件;Thread 从第一个 thread.started 事件里记下 thread id,下一轮就带上 resume <id>。模型、沙箱、审批策略等选项被翻译成命令行参数或 --config 覆盖;环境变量里注入 CODEX_INTERNAL_ORIGINATOR_OVERRIDE=codex_sdk_ts 标明调用方,传了 apiKey 就设 CODEX_API_KEY。进程以非零状态退出时,SDK 把收集到的 stderr 连同退出码抛成异常。可执行文件默认从 npm 包 @openai/codex 对应平台的子包里找。

Python SDK:直接讲 v2 协议

sdk/python 走的是另一条路:CodexClient 启动 codex app-server --listen stdio://,发 initialize(客户端名 codex_python_sdk,默认打开 experimentalApi),之后用 v2 方法建线程、开轮次。它的类型定义 generated/v2_all.py 就是从第 7 篇讲的 v2 JSON Schema 生成的 pydantic 模型。一个后台线程独占 stdout:带 method 和 id 的是服务端请求,交给审批处理函数并立即写回结果;只有 method 的是通知,按轮次分发;其余是响应。thread.run() 收集本轮的 item/completed 与用量通知,直到 turn/completed;最终回复优先取最后一条 phase 为 final_answer 的 agent 消息。

审批有两层。高层 API 的 approval_mode 默认是 auto_review,映射成审批策略 on-request 加自动审查人,由服务端的自动审查代为裁决;deny_all 映射成 never。低层 CodexClient 没传审批处理函数时,默认的处理函数会批准命令与文件修改:

    def _default_approval_handler(self, method: str, params: JsonObject | None) -> JsonObject:
        """Accept approval requests when the caller did not provide a handler."""
        if method == "item/commandExecution/requestApproval":
            return {"decision": "accept"}
        if method == "item/fileChange/requestApproval":
            return {"decision": "accept"}
        return {}

(sdk/python/src/openai_codex/client.py:833)

这与 codex exec 的一律拒绝正好相反,自己用 CodexClient 接入时值得留意。

和其它 agent 对照

Grok Build 的 Headless 模式 把 grok -p 称作“TUI 减去渲染”:复用 TUI 的会话启动与同一个会话 actor,只是不套界面。codex exec 是同一个思路,复用的层次更往外一层,是整个 app-server。OpenCode 的其他入口一篇里,opencode run 在本地走进程内的 fetch,与 codex exec 用进程内客户端访问同一套服务端的做法相近。


上一篇:会话持久化 · rollout、thread-store 与 SQLite · 下一篇:Session 与 TurnContext · 会话状态与每轮配置

本页目录