openinterpreter 实战:用 codex-core-api 的 ThreadManager 构建单轮 Codex 会话示例程序
本文围绕 openinterpreter 仓库中的 thread-manager-sample 示例展开,完整讲解这个"一次性"(one-shot)二进制的用法、启动流程与事件循环实现。读完你能掌握:如何在仅依赖 codex-core-api 一个门面 crate 的前提下,手工组装 Config、初始化 AuthManager/ThreadStore/EnvironmentManager 等运行时依赖、通过 ThreadManager 创建线程、提交单个用户回合(turn),并把事件流映射为 NDJSON 输出的完整集成路径。
1. 这个示例做什么
thread-manager-sample 是 codex-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> |
可选长选项 | 覆盖本次运行使用的模型,对应 Config 的 model 字段 |
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 参数则直接写入 Config 的 model 字段(第 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 节的事件循环里才会把 ExecApprovalRequest、ApplyPatchApprovalRequest、RequestPermissions 等事件一律视为错误直接 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,最终落入 Config 的 codex_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_home 由 find_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 返回 NewThread(thread_id + 线程句柄),SessionSource::Exec 表明该会话按 exec(非交互)来源登记。注意 CodexThread、NewThread、StartThreadOptions、ThreadManager 等类型都从 codex-core 经 codex-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/End、PatchApplyBegin、ExecCommandBegin/OutputDelta/End、ReasoningContentDelta、ItemStarted/Completed 等)统一交给 item_event_to_server_notification(event.msg.clone(), thread_id, current_turn_id) 映射为 ServerNotification,序列化后逐行写入 stdout 并立即 flush(main.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_output 与 shutdown_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-core、codex-exec-server 等 crate。这实际上是在用示例代码验证 codex-core-api 作为**编程门面(facade)**的完备性——从 lib.rs 可以看到,它聚合了 codex-core(ThreadManager、CodexThread、Config)、codex-exec-server(EnvironmentManager)、codex-login(AuthManager)、codex-config、codex-protocol(Op、EventMsg、UserInput)等多个底层 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. 小结:这个示例能带走的集成要点
- 最小启动清单:
Config→init_state_db→AuthManager→ThreadStore→EnvironmentManager→ExtensionRegistry(可选)→ThreadManager::new→start_thread,这条装配链即 main.rs#L115-L156 所展示的顺序; - 无人值守配置:
AskForApproval::Never+PermissionProfile::read_only()+ephemeral: true的组合适合脚本化单次调用,但任何审批类事件在事件循环里都应显式失败,而不是静默忽略; - 事件即协议:
EventMsg→item_event_to_server_notification的映射表明,TUI、app-server 等上层展示层与该示例共用同一套事件到通知的转换逻辑,示例输出的 NDJSON 可作为理解协议事件的直接样例; - 门面即边界:以
codex-core-api为唯一工作区依赖编写集成代码,是被这个示例明确示范并强制的工程约束。
相关延伸阅读:codex-rs/core-api/src/lib.rs(门面再导出全貌)、codex-rs/Cargo.toml(工作区成员列表)、codex-rs/thread-manager-sample/src/main.rs(完整实现)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00