首页
/ openinterpreter 实战:用 codex-core-api 的 ThreadManager 构建单轮 Codex 会话示例程序

openinterpreter 实战:用 codex-core-api 的 ThreadManager 构建单轮 Codex 会话示例程序

2026-09-06 16:20:16作者:曹令琨Iris

本文围绕 openinterpreter 仓库中的 thread-manager-sample 示例展开,完整讲解这个"一次性"(one-shot)二进制的用法、启动流程与事件循环实现。读完你能掌握:如何在仅依赖 codex-core-api 一个门面 crate 的前提下,手工组装 Config、初始化 AuthManager/ThreadStore/EnvironmentManager 等运行时依赖、通过 ThreadManager 创建线程、提交单个用户回合(turn),并把事件流映射为 NDJSON 输出的完整集成路径。

1. 这个示例做什么

thread-manager-samplecodex-rs 工作区中的一个小型示例 crate(Cargo.toml 中的包名为 codex-thread-manager-sample)。它的作用按 README 描述是:

  • 通过 codex-core-api 暴露的 ThreadManager 启动一个 Codex 线程(thread);
  • 提交单个用户回合(turn);
  • 输出最终结果。

src/main.rs 中 clap 命令的 about 描述可以更精确地理解其行为边界:该程序运行一个 Codex 回合,并把映射后的服务器通知(server notifications)以换行分隔的 JSON(NDJSON)形式打印出来。

2. 快速上手

README 给出了三种等价调用方式。注意 -- 之后才是传给示例程序本身的参数:

# 基本用法:提交一个提示词
cargo run -p codex-thread-manager-sample -- "Say hello"

# 用 --model 覆盖配置的默认模型
cargo run -p codex-thread-manager-sample -- --model gpt-5.2 "Say hello"

# 提示词也可以通过管道从 stdin 传入
printf 'Say hello\n' | cargo run -p codex-thread-manager-sample

2.1 CLI 参数解析

命令行接口由 main.rs 中的 Args 结构体 定义(基于 clap::Parser):

参数 形式 说明
--model <MODEL> 可选长选项 覆盖本次运行使用的模型,对应 Configmodel 字段
PROMPT... 位置参数 提示词文本,支持多个词段;内部会用空格 join(" ") 拼成一条提示
(省略位置参数) stdin 当未提供提示词时,程序要求输入必须是非终端(管道),否则直接报错 no prompt provided; pass a prompt argument or pipe one into stdin

提示词的读取与规范化逻辑在 run_main 中:

let prompt = if args.prompt.is_empty() {
    if std::io::stdin().is_terminal() {
        bail!("no prompt provided; pass a prompt argument or pipe one into stdin");
    }
    let mut prompt = String::new();
    std::io::stdin().read_to_string(&mut prompt)
        .context("read prompt from stdin")?;
    let prompt = prompt.replace("\r\n", "\n").replace('\r', "\n");
    // ...
    prompt
} else {
    args.prompt.join(" ")
};

可以看到从 stdin 读入的内容会做换行规范化(CRLF/CR 统一为 LF),且全空白输入同样会被拒绝。

3. 手工组装 Config:示例最核心的教学点

这个示例最大的价值在于它完全绕过了 CLI/TUI 的常规启动路径,在 new_config 中以结构体字面量方式手工构造了一个完整的 Config。对想理解"一个最小可运行 Codex 会话需要哪些配置"的读者来说,这份 150 行左右的字段清单本身就是参考资料。几个关键字段值得单独展开:

3.1 模型与模型提供方

let model_provider_id = OPENAI_PROVIDER_ID.to_string();
let model_providers = built_in_model_providers(/*openai_base_url*/ None);

提供方 ID 固定为 OPENAI_PROVIDER_ID,候选提供方列表来自 built_in_model_providers(两者均由 codex-core-api 再导出自 codex-model-provider-info,见 core-api/src/lib.rs)。随后从列表里取出 OpenAI 条目作为 model_provider--model 参数则直接写入 Configmodel 字段(第 191 行)。

3.2 权限策略:只读 + 从不询问

permissions: Permissions::from_approval_and_profile(
    Constrained::allow_any(AskForApproval::Never),
    Constrained::allow_any(PermissionProfile::read_only()),
)?,

main.rs#L200-L203)这是"无人值守一次性运行"的安全底线:审批策略为 AskForApproval::Never、权限档为只读(PermissionProfile::read_only())。正因为如此,第 4.3 节的事件循环里才会把 ExecApprovalRequestApplyPatchApprovalRequestRequestPermissions 等事件一律视为错误直接 bail!——在这个示例中,任何需要人工审批或权限提升的情况都没有继续执行的意义。

3.3 会话行为与存储

  • ephemeral: true(第 268 行):线程为临时会话,不写入持久会话历史,符合"跑完即弃"的示例定位;
  • experimental_thread_store: ThreadStoreConfig::Local:线程存储使用本地实现,与 init_state_db(&config) 建立的 SQLite 状态库相配合;
  • cwd / workspace_roots 取当前目录(第 178 行、第 239-240 行);
  • analytics_enabled: Some(false)otel: OtelConfig::default():关闭遥测与分析,保持输出纯净。

配置最后一步是把特性开关设为默认集:

config.features.set(Features::with_defaults())
    .context("configure default features")?;

3.4 程序入口:arg0 分发

main 函数只有一行(main.rs#L87-L91):

fn main() -> anyhow::Result<()> {
    arg0_dispatch_or_else(run_main)
}

arg0_dispatch_or_else 是 openinterpreter(Codex 内核)多入口二进制的分发机制:同一个可执行文件可能以不同 argv[0] 身份被调用,Arg0DispatchPaths 会把自身路径、Linux 沙箱可执行文件等运行时路径解析出来传给 run_main,最终落入 Configcodex_self_exe / codex_linux_sandbox_exe 字段。

4. 运行时装配与线程生命周期

run_main 展示了 ThreadManager::new 之前的完整依赖装配顺序,这正是把示例移植到自己项目时的清单。

4.1 依赖装配顺序

let config = new_config(args.model, arg0_paths)?;
let state_db = init_state_db(&config).await;              // SQLite 状态库
let auth_manager = AuthManager::shared_from_config(
    &config, /*enable_codex_api_key_env*/ false).await;   // 认证
let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
    config.codex_self_exe.clone(),
    config.codex_linux_sandbox_exe.clone(),
)?;
let thread_store = thread_store_from_config(&config, state_db.clone());
let environment_manager = Arc::new(EnvironmentManager::from_codex_home(
    config.codex_home.clone(),
    Some(local_runtime_paths),
    config.http_client_factory(),
).await?);
let installation_id = resolve_installation_id(&config.codex_home).await?;
let user_instructions_provider = Arc::new(
    CodexHomeUserInstructionsProvider::new(config.codex_home.clone()));
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
install_image_generation_extension(&mut extensions, auth_manager.clone(), |config| {
    Some(config.codex_home.clone())
});

其中 codex_homefind_codex_home(从 codex-core-api 再导出自 codex-core)解析,CodexHomeUserInstructionsProvider 则负责从该目录加载用户级指令(如 AGENTS.md 类文件)。扩展注册这里只装了 install_image_generation_extension 一个扩展,说明对最小示例而言扩展注册并非必须项,但接口已经预留。

4.2 创建 ThreadManager 并启动线程

ThreadManager::new 调用 一次性注入 14 个依赖:

let thread_manager = ThreadManager::new(
    &config,
    Arc::clone(&auth_manager),
    build_models_manager(&config, auth_manager),
    CodexAppsToolsCache::default(),
    SessionSource::Exec,
    environment_manager,
    Arc::new(extensions.build()),
    user_instructions_provider,
    /*analytics_events_client*/ None,
    Arc::clone(&thread_store),
    local_agent_graph_store_from_state_db(state_db.as_ref()),
    installation_id,
    /*attestation_provider*/ None,
    /*external_time_provider*/ None,
);

let NewThread { thread_id, thread, .. } = thread_manager
    .start_thread(StartThreadOptions::new(config))
    .await
    .context("start Codex thread")?;

start_thread 返回 NewThreadthread_id + 线程句柄),SessionSource::Exec 表明该会话按 exec(非交互)来源登记。注意 CodexThreadNewThreadStartThreadOptionsThreadManager 等类型都从 codex-corecodex-core-api 门面 再导出——这正是示例刻意保持"单一工作区依赖"的基础。

4.3 提交回合与事件循环

run_turn 是理解 Codex 事件模型的浓缩样本。第一步用 Op::UserInput 提交用户输入:

thread.submit(Op::UserInput {
    items: vec![UserInput::Text {
        text: prompt,
        text_elements: Vec::new(),
    }],
    final_output_json_schema: None,
    responsesapi_client_metadata: None,
    additional_context: Default::default(),
    thread_settings: Default::default(),
}).await.context("submit user input")?;

随后进入 loop,从 thread.next_event() 拉取 EventMsg 并做两件事:

(1)把可映射事件转换为服务器通知并输出 NDJSON。 TurnStarted 先被单独捕获以记录 turn_id;其余约 30 种事件(如 McpToolCallBegin/EndPatchApplyBeginExecCommandBegin/OutputDelta/EndReasoningContentDeltaItemStarted/Completed 等)统一交给 item_event_to_server_notification(event.msg.clone(), thread_id, current_turn_id) 映射为 ServerNotification,序列化后逐行写入 stdout 并立即 flushmain.rs#L388-L394)。item_event_to_server_notification 来自 codex-app-server-protocol,同样经 codex-core-api 导出(lib.rs#L6-L7)。如果映射事件先于 TurnStarted 到达,程序会直接报 mapped notification arrived before turn started 错误,说明 turn_id 是该通知协议的前置条件。

(2)处理终止与异常事件。 事件循环定义了明确的退出语义(main.rs#L397-L423):

事件 处理
TurnComplete 正常返回,结束回合
Error / TurnAborted 以错误消息 bail!
ExecApprovalRequest / ApplyPatchApprovalRequest bail!("turn requested exec/patch approval")
RequestPermissions / RequestUserInput / DynamicToolCallRequest bail!(本示例无人工交互通道)

这与 3.2 节的权限配置呼应:只读 + 从不审批的配置下,任何需要人工介入的事件都被视为运行失败。

4.4 线程关闭与清理

回合结束后 run_main 的收尾 依次执行:

let turn_output = run_turn(&thread, &thread_id_string, prompt).await;
let shutdown_result = thread.shutdown_and_wait().await;
let _ = thread_manager.remove_thread(&thread_id).await;

turn_output?;
shutdown_result.context("shut down Codex thread")?;

先等待回合完成,再 shutdown_and_wait 等待线程资源释放,最后从管理器中移除线程。注意错误传播顺序:turn_outputshutdown_result 都被显式检查,关闭失败不会因 remove_thread 的结果而被掩盖(remove_thread 的错误被有意忽略,因为此时线程已不可用属于可预期情况)。

5. 依赖与构建体系

5.1 "单一工作区依赖"的设计约束

Cargo.toml 的依赖声明带有明确的架构意图:

[dependencies]
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
# Keep this sample limited to a single Codex workspace dependency.
# Add new Codex surface area to `codex-core-api` instead of depending on
# additional `codex-*` crates here.
codex-core-api = { workspace = true }
tracing = { workspace = true }

即:这个示例只允许依赖 codex-core-api 这一个 Codex 工作区 crate;如果需要用到新的能力,正确做法是把相应 API 加进 codex-core-api 的再导出清单,而不是在此直接引入 codex-corecodex-exec-server 等 crate。这实际上是在用示例代码验证 codex-core-api 作为**编程门面(facade)**的完备性——从 lib.rs 可以看到,它聚合了 codex-coreThreadManagerCodexThreadConfig)、codex-exec-serverEnvironmentManager)、codex-loginAuthManager)、codex-configcodex-protocolOpEventMsgUserInput)等多个底层 crate 的公开类型。对想在自己的 Rust 程序中内嵌 Codex 能力、又不想直面几十个内部 crate 的集成者,这份再导出面就是官方支持的接入点。

5.2 Bazel 构建

该 crate 同时提供 Bazel 构建入口,BUILD.bazel 仅三行核心声明:

codex_rust_crate(
    name = "thread-manager-sample",
    crate_name = "codex_thread_manager_sample",
)

crate 名(下划线形式)与 Cargo 包名(连字符形式)一一对应;构建规则 codex_rust_crate 定义在仓库根的 defs.bzl 中,整个 codex-rs 目录采用统一的规则封装,便于在无 Cargo 缓存的 CI 环境中确定性构建。

6. 小结:这个示例能带走的集成要点

  1. 最小启动清单Configinit_state_dbAuthManagerThreadStoreEnvironmentManagerExtensionRegistry(可选)→ ThreadManager::newstart_thread,这条装配链即 main.rs#L115-L156 所展示的顺序;
  2. 无人值守配置AskForApproval::Never + PermissionProfile::read_only() + ephemeral: true 的组合适合脚本化单次调用,但任何审批类事件在事件循环里都应显式失败,而不是静默忽略;
  3. 事件即协议EventMsgitem_event_to_server_notification 的映射表明,TUI、app-server 等上层展示层与该示例共用同一套事件到通知的转换逻辑,示例输出的 NDJSON 可作为理解协议事件的直接样例;
  4. 门面即边界:以 codex-core-api 为唯一工作区依赖编写集成代码,是被这个示例明确示范并强制的工程约束。

相关延伸阅读:codex-rs/core-api/src/lib.rs(门面再导出全貌)、codex-rs/Cargo.toml(工作区成员列表)、codex-rs/thread-manager-sample/src/main.rs(完整实现)。

登录后查看全文
热门项目推荐
相关项目推荐